├── .editorconfig ├── .github └── workflows │ ├── build.yaml │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── LICENSE ├── README.md ├── csharp-language-server.sln ├── global.json ├── src └── CSharpLanguageServer │ ├── CHANGELOG.md │ ├── CSharpLanguageServer.fsproj │ ├── Conversions.fs │ ├── DocumentationUtil.fs │ ├── FormatUtil.fs │ ├── Handlers │ ├── CSharpMetadata.fs │ ├── CallHierarchy.fs │ ├── CodeAction.fs │ ├── CodeLens.fs │ ├── Color.fs │ ├── Completion.fs │ ├── Declaration.fs │ ├── Definition.fs │ ├── Diagnostic.fs │ ├── DocumentFormatting.fs │ ├── DocumentHighlight.fs │ ├── DocumentLink.fs │ ├── DocumentOnTypeFormatting.fs │ ├── DocumentRangeFormatting.fs │ ├── DocumentSymbol.fs │ ├── ExecuteCommand.fs │ ├── FoldingRange.fs │ ├── Hover.fs │ ├── Implementation.fs │ ├── Initialization.fs │ ├── InlayHint.fs │ ├── InlineValue.fs │ ├── LinkedEditingRange.fs │ ├── Moniker.fs │ ├── References.fs │ ├── Rename.fs │ ├── SelectionRange.fs │ ├── SemanticTokens.fs │ ├── SignatureHelp.fs │ ├── TextDocumentSync.fs │ ├── TypeDefinition.fs │ ├── TypeHierarchy.fs │ ├── Workspace.fs │ └── WorkspaceSymbol.fs │ ├── Logging.fs │ ├── LruCache.fs │ ├── Lsp │ ├── Client.fs │ └── Server.fs │ ├── Options.fs │ ├── Program.fs │ ├── ProgressReporter.fs │ ├── README.md │ ├── RoslynHelpers.fs │ ├── State │ ├── ServerRequestContext.fs │ └── ServerState.fs │ ├── Types.fs │ └── Util.fs └── tests └── CSharpLanguageServer.Tests ├── CSharpLanguageServer.Tests.fsproj ├── CodeActionTests.fs ├── DefinitionTests.fs ├── DiagnosticTests.fs ├── DocumentFormattingTests.fs ├── DocumentationTests.fs ├── HoverTests.fs ├── InitializationTests.fs ├── InternalTests.fs ├── ReferenceTests.fs ├── TestData ├── testCodeActionOnMethodNameWorks │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testDefinitionWorks │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testDefinitionWorksInAspNetProject │ └── Project │ │ ├── Controllers │ │ └── TestController.cs │ │ ├── Models │ │ └── Test │ │ │ └── IndexViewModel.cs │ │ ├── Program.cs │ │ ├── Project.csproj │ │ ├── Startup.cs │ │ └── Views │ │ ├── Test │ │ └── Index.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml ├── testEditorConfigFormatting │ └── Project │ │ ├── .editorconfig │ │ ├── Class.cs │ │ ├── ExpectedFormatting.cs.txt │ │ └── Project.csproj ├── testHoverWorks │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testMultiTargetProjectLoads │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testPullDiagnosticsWork │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testPushDiagnosticsWork │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testReferenceWorksDotnet8 │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testReferenceWorksDotnet9 │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj ├── testReferenceWorksToAspNetRazorPageReferencedValue │ └── Project │ │ ├── Controllers │ │ └── TestController.cs │ │ ├── Models │ │ └── Test │ │ │ └── IndexViewModel.cs │ │ ├── Program.cs │ │ ├── Project.csproj │ │ ├── Startup.cs │ │ └── Views │ │ ├── Test │ │ └── Index.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml ├── testServerRegistersCapabilitiesWithTheClient │ └── Project │ │ ├── Class.cs │ │ └── Project.csproj └── testSlnx │ ├── Project │ ├── Class.cs │ └── Project.csproj │ └── Test.slnx └── Tooling.fs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | max_line_length = 120 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.fs] 12 | fsharp_newline_before_multiline_computation_expression = false 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: {} 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-24.04] 12 | dotnet: [9.0.x] 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: ${{ matrix.dotnet }} 23 | 24 | - name: Restore tools 25 | run: dotnet tool restore 26 | 27 | - name: Run Build 28 | run: dotnet build 29 | 30 | - name: Run publish 31 | run: dotnet pack -c Release -o release 32 | working-directory: src/CSharpLanguageServer 33 | 34 | - name: Upload NuGet packages as artifacts 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: packages 38 | path: src/CSharpLanguageServer/release/ 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [windows-latest, ubuntu-24.04] 13 | dotnet: [9.0.x] 14 | fail-fast: false 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: ${{ matrix.dotnet }} 26 | 27 | - name: Restore tools 28 | run: dotnet tool restore 29 | 30 | - name: Run Build 31 | run: dotnet build 32 | 33 | - name: Run Tests 34 | run: dotnet test --no-build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .ionide/ 4 | .idea/ 5 | .config/ 6 | nupkg/ 7 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | csharp-ls 5 | csharp-ls 6 | MIT 7 | $(MSBuildThisFileDirectory)CHANGELOG.md 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | true 6 | 7 | 8 | 9 | 17.14.8 10 | 4.14.0 11 | 1.9.1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2021 Saulius Menkevičius 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | This is a hacky Roslyn-based LSP server for C#, as an alternative to 3 | [omnisharp-roslyn](https://github.com/OmniSharp/omnisharp-roslyn). 4 | 5 | `csharp-ls` requires .NET 8 SDK to be installed. However it has been reported 6 | to work with projects using older versions of dotnet SDK, including .NET Core 3, 7 | .NET Framework 4.8 and possibly older ones too as it uses the standard 8 | Roslyn/MSBuild libs that Visual Studio & omnisharp does. 9 | 10 | See [CHANGELOG.md](CHANGELOG.md) for the list of recent improvements/fixes. 11 | 12 | # Acknowledgements 13 | - csharp-ls is not affiliated with Microsoft Corp; 14 | - csharp-ls uses LSP interface from [Ionide.LanguageServerProtocol](https://github.com/ionide/LanguageServerProtocol); 15 | - csharp-ls uses [Roslyn](https://github.com/dotnet/roslyn) to parse and update code; Roslyn maps really nicely to LSP w/relatively little impedance mismatch; 16 | - csharp-ls uses [ILSpy/ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy) to decompile types in assemblies to C# source. 17 | 18 | # Installation 19 | `dotnet tool install --global csharp-ls` 20 | 21 | See [csharp-ls nuget page](https://www.nuget.org/packages/csharp-ls/) 22 | 23 | # Settings 24 | 25 | - `csharp.solution` - solution to load, optional 26 | - `csharp.applyFormattingOptions` - use formatting options as supplied by the client (may override `.editorconfig` values), defaults to `false` 27 | 28 | # Clients 29 | 30 | `csharp-ls` implements the standard LSP protocol to interact with your editor. 31 | However there are some features that need a non-standard implementation and this 32 | is where editor-specific plugins can be helpful. 33 | 34 | ## Emacs 35 | ### emacs/lsp-mode 36 | Supports automatic installation, go-to-metatada (can view code from nuget/compiled dlls) 37 | and some additional features. 38 | 39 | See [emacs/lsp-mode](https://github.com/emacs-lsp/lsp-mode). 40 | 41 | ## Visual Studio Code 42 | ### vytautassurvila/vscode-csharp-ls 43 | - Supports code decompilation from metadata 44 | 45 | See [csharp-ls](https://marketplace.visualstudio.com/items?itemName=vytautassurvila.csharp-ls) and [vscode-csharp-ls @ github](https://github.com/vytautassurvila/vscode-csharp-ls). 46 | 47 | ### statiolake/vscode-csharp-ls 48 | See [vscode-csharp-ls](https://marketplace.visualstudio.com/items?itemName=statiolake.vscode-csharp-ls). 49 | 50 | # Building 51 | 52 | ## On Linux/macOS 53 | 54 | ``` 55 | $ dotnet build 56 | ``` 57 | 58 | # FAQ 59 | 60 | ## decompile for your editor , with the example of neovim 61 | 62 | ### api 63 | 64 | The api is "csharp/metadata", in neovim ,you can request it like 65 | 66 | ```lua 67 | local result, err = client.request_sync("csharp/metadata", params, 10000) 68 | ``` 69 | 70 | #### sender 71 | You need to send a uri, it is like 72 | 73 | **csharp:/metadata/projects/trainning2/assemblies/System.Console/symbols/System.Console.cs** 74 | 75 | In neovim, it will be result(s) from vim.lsp.handles["textDocument/definition"] 76 | 77 | and the key of uri is the key, 78 | 79 | The key to send is like 80 | 81 | ```lua 82 | local params = { 83 | timeout = 5000, 84 | textDocument = { 85 | uri = uri, 86 | } 87 | } 88 | ``` 89 | 90 | The key of textDocument is needed. And timeout is just for neovim. It is the same if is expressed by json. 91 | 92 | ### receiver 93 | 94 | The object received is like 95 | 96 | ```lua 97 | { 98 | projectName = "csharp-test", 99 | assemblyName = "System.Runtime", 100 | symbolName = "System.String", 101 | source = "using System.Buffers;\n ...." 102 | } 103 | ``` 104 | 105 | And In neovim, You receive the "result" above, you can get the decompile source from 106 | 107 | ```lua 108 | 109 | local result, err = client.request_sync("csharp/metadata", params, 10000) 110 | local source 111 | if not err then 112 | source = result.result.source 113 | end 114 | ``` 115 | 116 | And there is a plugin of neovim for you to decompile it. 117 | 118 | [csharpls-extended-lsp.nvim](https://github.com/chen244/csharpls-extended-lsp.nvim) 119 | -------------------------------------------------------------------------------- /csharp-language-server.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3ADA171B-D93F-4D34-97EA-5BB06662678B}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CSharpLanguageServer", "src\CSharpLanguageServer\CSharpLanguageServer.fsproj", "{228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{35DF1A49-B49A-42CB-B43B-8E8120AD9B0E}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CSharpLanguageServer.Tests", "tests\CSharpLanguageServer.Tests\CSharpLanguageServer.Tests.fsproj", "{754F8FCE-6DC2-43E2-832D-00A8B830D192}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Debug|x64 = Debug|x64 21 | Debug|x86 = Debug|x86 22 | Release|Any CPU = Release|Any CPU 23 | Release|x64 = Release|x64 24 | Release|x86 = Release|x86 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Debug|x64.ActiveCfg = Debug|Any CPU 30 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Debug|x64.Build.0 = Debug|Any CPU 31 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Debug|x86.ActiveCfg = Debug|Any CPU 32 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Debug|x86.Build.0 = Debug|Any CPU 33 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Release|x64.ActiveCfg = Release|Any CPU 36 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Release|x64.Build.0 = Release|Any CPU 37 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Release|x86.ActiveCfg = Release|Any CPU 38 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02}.Release|x86.Build.0 = Release|Any CPU 39 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Debug|x64.ActiveCfg = Debug|Any CPU 42 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Debug|x64.Build.0 = Debug|Any CPU 43 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Debug|x86.ActiveCfg = Debug|Any CPU 44 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Debug|x86.Build.0 = Debug|Any CPU 45 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|x64.ActiveCfg = Release|Any CPU 48 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|x64.Build.0 = Release|Any CPU 49 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|x86.ActiveCfg = Release|Any CPU 50 | {754F8FCE-6DC2-43E2-832D-00A8B830D192}.Release|x86.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {228A59AD-2E4C-4DF4-AC5E-1F126A4FFF02} = {3ADA171B-D93F-4D34-97EA-5BB06662678B} 54 | {754F8FCE-6DC2-43E2-832D-00A8B830D192} = {35DF1A49-B49A-42CB-B43B-8E8120AD9B0E} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.1", 4 | "rollForward": "minor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /src/CSharpLanguageServer/CSharpLanguageServer.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Exe 6 | net9.0 7 | C# LSP language server 8 | true 9 | csharp-ls 10 | Saulius Menkevičius 11 | ./nupkg 12 | csharp-ls 13 | MIT 14 | csharp;lsp;roslyn 15 | https://github.com/razzmatazz/csharp-language-server 16 | true 17 | README.md 18 | CHANGELOG.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | NU1604 89 | NU1701 90 | 91 | 92 | NU1604 93 | NU1701 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/DocumentationUtil.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer 2 | 3 | open System 4 | open System.Xml.Linq 5 | 6 | open Microsoft.CodeAnalysis 7 | 8 | open CSharpLanguageServer.Conversions 9 | 10 | module DocumentationUtil = 11 | type TripleSlashComment = 12 | { Summary: XElement list 13 | Params: (string * XElement) list 14 | Exceptions: (string * XElement) list 15 | Returns: XElement list 16 | Types: (string * XElement) list 17 | Remarks: XElement list 18 | OtherLines: XElement list } 19 | static member Default = 20 | { Summary = [] 21 | Params = [] 22 | Exceptions = [] 23 | Returns = [] 24 | Types = [] 25 | Remarks = [] 26 | OtherLines = [] } 27 | 28 | let parseCref (cref: string) = 29 | let parts = cref.Split(':') 30 | match parts.Length with 31 | | 1 -> cref 32 | | _ -> String.Join(":", parts |> Seq.skip 1) 33 | 34 | let normalizeWhitespace (s: string) = 35 | let mutable modified = s 36 | let mutable prevModified = "" 37 | while modified <> prevModified do 38 | prevModified <- modified 39 | modified <- modified.Replace(" ", " ").Replace("\r\n", " ").Replace("\n", " ") 40 | 41 | modified 42 | 43 | let formatTextElement (n: XElement) = 44 | 45 | let formatSeeElement (e: XElement) = 46 | let crefMaybe = 47 | e.Attribute(XName.Get("cref")) 48 | |> Option.ofObj 49 | |> Option.map (fun x -> x.Value) 50 | |> Option.map parseCref 51 | |> Option.map (fun s -> sprintf "``%s``" s) 52 | |> Option.toList 53 | 54 | let langWordMaybe = 55 | e.Attribute(XName.Get("langword")) 56 | |> Option.ofObj 57 | |> Option.map (fun x -> sprintf "``%s``" x.Value) 58 | |> Option.toList 59 | 60 | crefMaybe |> Seq.append langWordMaybe |> List.ofSeq 61 | 62 | let rec formatTextNode (subnode: XNode) = 63 | match subnode with 64 | | :? XElement as e -> 65 | match e.Name.LocalName with 66 | | "c" -> [ sprintf "``%s``" e.Value ] 67 | | "see" -> formatSeeElement e 68 | | "paramref" -> 69 | e.Attribute(XName.Get("name")) 70 | |> Option.ofObj 71 | |> Option.map (fun x -> sprintf "``%s``" x.Value) 72 | |> Option.toList 73 | | "para" -> 74 | e.Nodes() 75 | |> Seq.collect formatTextNode 76 | |> Seq.append [ "\n\n" ] 77 | |> List.ofSeq 78 | | _ -> [ e.Value ] 79 | | :? XText as t -> [ t.Value |> normalizeWhitespace ] 80 | | _ -> [] 81 | 82 | n.Nodes() |> Seq.collect formatTextNode |> (fun ss -> String.Join("", ss)) 83 | 84 | let extendCommentWithElement comment (n: XElement) = 85 | match n.Name.LocalName with 86 | | "summary" -> 87 | let newSummary = comment.Summary |> List.append [ n ] 88 | { comment with Summary = newSummary } 89 | 90 | | "remarks" -> 91 | let newRemarks = comment.Remarks |> List.append [ n ] 92 | { comment with Remarks = newRemarks } 93 | 94 | | "param" -> 95 | let name = n.Attribute(XName.Get("name")).Value 96 | { comment with Params = comment.Params |> List.append [ (name, n) ] } 97 | 98 | | "returns" -> 99 | { comment with Returns = comment.Returns |> List.append [ n ] } 100 | 101 | | "exception" -> 102 | let name = n.Attribute(XName.Get("cref")).Value |> parseCref 103 | { comment with 104 | Exceptions = comment.Exceptions |> List.append [ (name, n) ] } 105 | 106 | | "typeparam" -> 107 | let name = n.Attribute(XName.Get("name")).Value 108 | { comment with Types = comment.Types |> List.append [ (name, n) ] } 109 | 110 | | _ -> 111 | { comment with OtherLines = comment.OtherLines |> List.append [ n ] } 112 | 113 | 114 | let parseComment xmlDocumentation: TripleSlashComment = 115 | let doc = XDocument.Parse("" + xmlDocumentation + "") 116 | 117 | let unwrapDocRoot (root: XElement) = 118 | let elementNames (el: XElement) = 119 | el.Elements() |> Seq.map (fun e -> e.Name.LocalName) |> List.ofSeq 120 | 121 | match elementNames root with 122 | | [ "member" ] -> root.Element(XName.Get("member")) 123 | | _ -> root 124 | 125 | doc.Root 126 | |> unwrapDocRoot 127 | |> fun r -> r.Elements() 128 | |> Seq.fold extendCommentWithElement TripleSlashComment.Default 129 | 130 | 131 | let formatComment model : string list = 132 | 133 | let appendNamed name (kvs: (string * XElement) seq) markdownLines = 134 | match Seq.isEmpty kvs with 135 | | true -> markdownLines 136 | | false -> 137 | let formatItem (key, value) = 138 | sprintf "- ``%s``: %s" key (formatTextElement value) 139 | 140 | markdownLines 141 | |> List.append [ name + ":"; "" ] 142 | |> List.append (kvs |> Seq.map formatItem |> List.ofSeq) 143 | 144 | let appendFormatted name elms markdownLines = 145 | let formattedLines = 146 | elms 147 | |> List.map formatTextElement 148 | |> List.filter (not << String.IsNullOrWhiteSpace) 149 | 150 | match Seq.isEmpty formattedLines with 151 | | true -> markdownLines 152 | | false -> 153 | markdownLines 154 | |> List.append [ "" ] 155 | |> List.append (formattedLines |> List.map (fun s -> name + ": " + s)) 156 | 157 | [] 158 | |> List.append (model.Summary |> List.map formatTextElement) 159 | |> appendNamed "Parameters" model.Params 160 | |> appendFormatted "Returns" model.Returns 161 | |> appendNamed "Exceptions" model.Exceptions 162 | |> appendNamed "Types" model.Types 163 | |> appendFormatted "Remarks" model.Remarks 164 | |> List.append (model.OtherLines |> List.map string) 165 | |> List.rev 166 | |> List.map (fun s -> s.Trim()) 167 | 168 | 169 | let formatDocXml xmlDocumentation = 170 | String.Join("\n", xmlDocumentation |> parseComment |> formatComment) 171 | 172 | let markdownDocForSymbol (sym: ISymbol) = 173 | let comment = parseComment (sym.GetDocumentationCommentXml()) 174 | let formattedDocLines = formatComment comment 175 | 176 | formattedDocLines |> (fun ss -> String.Join("\n", ss)) 177 | 178 | let markdownDocForSymbolWithSignature (sym: ISymbol) = 179 | let symbolName = SymbolName.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat sym 180 | 181 | let symbolInfoLines = 182 | match symbolName with 183 | | "" -> [] 184 | | typeName -> [ sprintf "```csharp\n%s\n```" typeName ] 185 | 186 | let comment = parseComment (sym.GetDocumentationCommentXml()) 187 | let formattedDocLines = formatComment comment 188 | 189 | formattedDocLines 190 | |> Seq.append ( 191 | if symbolInfoLines.Length > 0 && formattedDocLines.Length > 0 then 192 | [ "" ] 193 | else 194 | [] 195 | ) 196 | |> Seq.append symbolInfoLines 197 | |> (fun ss -> String.Join("\n", ss)) 198 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/FormatUtil.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer 2 | 3 | open Microsoft.CodeAnalysis 4 | open Microsoft.CodeAnalysis.CSharp 5 | open Microsoft.CodeAnalysis.CSharp.Formatting 6 | open Microsoft.CodeAnalysis.Options 7 | open Microsoft.CodeAnalysis.Text 8 | open Microsoft.CodeAnalysis.Formatting 9 | open Ionide.LanguageServerProtocol.Types 10 | 11 | open CSharpLanguageServer.Types 12 | 13 | module internal FormatUtil = 14 | let processChange (oldText: SourceText) (change: TextChange) : TextEdit = 15 | let mapToTextEdit (linePosition: LinePositionSpan, newText: string) : TextEdit = 16 | { NewText = newText 17 | Range = 18 | { Start = 19 | { Line = uint32 linePosition.Start.Line 20 | Character = uint32 linePosition.Start.Character } 21 | End = 22 | { Line = uint32 linePosition.End.Line 23 | Character = uint32 linePosition.End.Character } } } 24 | 25 | let defaultTextEdit (oldText: SourceText, change: TextChange) : TextEdit = 26 | let linePosition = oldText.Lines.GetLinePositionSpan change.Span 27 | mapToTextEdit (linePosition, change.NewText) 28 | 29 | let padLeft (span: TextSpan) : TextSpan = 30 | TextSpan.FromBounds(span.Start - 1, span.End) 31 | 32 | let padRight (span: TextSpan) : TextSpan = 33 | TextSpan.FromBounds(span.Start, span.End + 1) 34 | 35 | let rec checkSpanLineEndings (newText: string, oldText: SourceText, span: TextSpan, prefix: string) : TextEdit = 36 | if 37 | span.Start > 0 38 | && newText[0].Equals('\n') 39 | && oldText[span.Start - 1].Equals('\r') 40 | then 41 | checkSpanLineEndings (newText, oldText, padLeft (span), "\r") |> ignore 42 | 43 | if 44 | span.End < oldText.Length - 1 45 | && newText[newText.Length - 1].Equals('\r') 46 | && oldText[span.End].Equals('\n') 47 | then 48 | let linePosition = oldText.Lines.GetLinePositionSpan(padRight (span)) 49 | mapToTextEdit (linePosition, (prefix + newText.ToString() + "\n")) 50 | else 51 | let linePosition = oldText.Lines.GetLinePositionSpan span 52 | mapToTextEdit (linePosition, newText.ToString()) 53 | 54 | let newText = change.NewText 55 | 56 | if newText.Length > 0 then 57 | checkSpanLineEndings (newText, oldText, change.Span, "") 58 | else 59 | defaultTextEdit (oldText, change) 60 | 61 | let convert (oldText: SourceText) (changes: TextChange[]) : TextEdit[] = 62 | //why doesnt it pick up that TextSpan implements IComparable? 63 | //one of life's many mysteries 64 | let comparer (lhs: TextChange) (rhs: TextChange) : int = lhs.Span.CompareTo(rhs.Span) 65 | 66 | changes 67 | |> Seq.sortWith comparer 68 | |> Seq.map (fun x -> processChange oldText x) 69 | |> Seq.toArray 70 | 71 | let getChanges (doc: Document) (oldDoc: Document) : Async = async { 72 | let! ct = Async.CancellationToken 73 | let! changes = doc.GetTextChangesAsync(oldDoc, ct) |> Async.AwaitTask 74 | let! oldText = oldDoc.GetTextAsync(ct) |> Async.AwaitTask 75 | return convert oldText (changes |> Seq.toArray) 76 | } 77 | 78 | let getFormattingOptions (settings: ServerSettings) (doc: Document) (formattingOptions: FormattingOptions) : Async = async { 79 | let! docOptions = doc.GetOptionsAsync() |> Async.AwaitTask 80 | 81 | return 82 | match settings.ApplyFormattingOptions with 83 | | false -> 84 | docOptions 85 | | true -> 86 | docOptions 87 | |> _.WithChangedOption(FormattingOptions.IndentationSize, LanguageNames.CSharp, int formattingOptions.TabSize) 88 | |> _.WithChangedOption(FormattingOptions.UseTabs, LanguageNames.CSharp, not formattingOptions.InsertSpaces) 89 | |> match formattingOptions.InsertFinalNewline with 90 | | Some insertFinalNewline -> 91 | _.WithChangedOption(CSharpFormattingOptions.NewLineForFinally, insertFinalNewline) 92 | | None -> id 93 | |> match formattingOptions.TrimFinalNewlines with 94 | | Some trimFinalNewlines -> 95 | _.WithChangedOption(CSharpFormattingOptions.NewLineForFinally, not trimFinalNewlines) 96 | | None -> id 97 | } 98 | 99 | let rec getSyntaxNode (token: SyntaxToken) : SyntaxNode option = 100 | if token.IsKind(SyntaxKind.EndOfFileToken) then 101 | getSyntaxNode (token.GetPreviousToken()) 102 | else 103 | match token.Kind() with 104 | | SyntaxKind.SemicolonToken -> token.Parent |> Some 105 | | SyntaxKind.CloseBraceToken -> 106 | let parent = token.Parent 107 | match parent.Kind() with 108 | | SyntaxKind.Block -> parent.Parent |> Some 109 | | _ -> parent |> Some 110 | | SyntaxKind.CloseParenToken -> 111 | if 112 | token.GetPreviousToken().IsKind(SyntaxKind.SemicolonToken) 113 | && token.Parent.IsKind(SyntaxKind.ForStatement) 114 | then 115 | token.Parent |> Some 116 | else 117 | None 118 | | _ -> None 119 | 120 | let findFormatTarget (root: SyntaxNode) (position: int) : SyntaxNode option = 121 | let token = root.FindToken position 122 | getSyntaxNode token 123 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/CSharpMetadata.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.Types 7 | open CSharpLanguageServer.State 8 | 9 | [] 10 | module CSharpMetadata = 11 | let handle (context: ServerRequestContext) (metadataParams: CSharpMetadataParams): AsyncLspResult = async { 12 | let uri = metadataParams.TextDocument.Uri 13 | 14 | let metadataMaybe = 15 | context.DecompiledMetadata 16 | |> Map.tryFind uri 17 | |> Option.map (fun x -> x.Metadata) 18 | 19 | return metadataMaybe |> LspResult.success 20 | } 21 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/CallHierarchy.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis 6 | open Microsoft.CodeAnalysis.FindSymbols 7 | open Ionide.LanguageServerProtocol.Server 8 | open Ionide.LanguageServerProtocol.Types 9 | open Ionide.LanguageServerProtocol.JsonRpc 10 | 11 | open CSharpLanguageServer.Types 12 | open CSharpLanguageServer.State 13 | open CSharpLanguageServer.Conversions 14 | 15 | [] 16 | module CallHierarchy = 17 | let private isCallableSymbol (symbol: ISymbol): bool = 18 | if isNull symbol then 19 | false 20 | else 21 | List.contains 22 | symbol.Kind 23 | [ Microsoft.CodeAnalysis.SymbolKind.Method 24 | Microsoft.CodeAnalysis.SymbolKind.Field 25 | Microsoft.CodeAnalysis.SymbolKind.Event 26 | Microsoft.CodeAnalysis.SymbolKind.Property ] 27 | 28 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 29 | clientCapabilities.TextDocument 30 | |> Option.bind (fun x -> x.CallHierarchy) 31 | |> Option.bind (fun x -> x.DynamicRegistration) 32 | |> Option.defaultValue false 33 | 34 | let provider (clientCapabilities: ClientCapabilities) : U3 option = 35 | match dynamicRegistration clientCapabilities with 36 | | true -> None 37 | | false -> Some (U3.C1 true) 38 | 39 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 40 | match dynamicRegistration clientCapabilities with 41 | | false -> None 42 | | true -> 43 | let registerOptions: CallHierarchyRegistrationOptions = 44 | { DocumentSelector = Some defaultDocumentSelector 45 | Id = None 46 | WorkDoneProgress = None 47 | } 48 | Some 49 | { Id = Guid.NewGuid().ToString() 50 | Method = "textDocument/prepareCallHierarchy" 51 | RegisterOptions = registerOptions |> serialize |> Some } 52 | 53 | let prepare (context: ServerRequestContext) (p: CallHierarchyPrepareParams) : AsyncLspResult = async { 54 | match! context.FindSymbol p.TextDocument.Uri p.Position with 55 | | Some symbol when isCallableSymbol symbol -> 56 | let! itemList = CallHierarchyItem.fromSymbol context.ResolveSymbolLocations symbol 57 | return 58 | itemList 59 | |> List.toArray 60 | |> Some 61 | |> LspResult.success 62 | | _ -> return None |> LspResult.success 63 | } 64 | 65 | let incomingCalls 66 | (context: ServerRequestContext) 67 | (p: CallHierarchyIncomingCallsParams) 68 | : AsyncLspResult = async { 69 | let toCallHierarchyIncomingCalls (info: SymbolCallerInfo) : CallHierarchyIncomingCall seq = 70 | let fromRanges = 71 | info.Locations 72 | |> Seq.map (fun l -> l.GetLineSpan().Span |> Range.fromLinePositionSpan) 73 | |> Seq.toArray 74 | 75 | info.CallingSymbol.Locations 76 | |> Seq.map Location.fromRoslynLocation 77 | |> Seq.filter _.IsSome 78 | |> Seq.map _.Value 79 | |> Seq.map (fun loc -> 80 | { From = CallHierarchyItem.fromSymbolAndLocation (info.CallingSymbol) loc 81 | FromRanges = fromRanges }) 82 | 83 | match! context.FindSymbol p.Item.Uri p.Item.Range.Start with 84 | | None -> return None |> LspResult.success 85 | | Some symbol -> 86 | let! callers = context.FindCallers symbol 87 | // TODO: If we remove info.IsDirect, then we will get lots of false positive. But if we keep it, 88 | // we will miss many callers. Maybe it should have some change in LSP protocol. 89 | return 90 | callers 91 | |> Seq.filter (fun info -> info.IsDirect && isCallableSymbol info.CallingSymbol) 92 | |> Seq.collect toCallHierarchyIncomingCalls 93 | |> Seq.distinct 94 | |> Seq.toArray 95 | |> Some 96 | |> LspResult.success 97 | } 98 | 99 | let outgoingCalls 100 | (_context: ServerRequestContext) 101 | (_: CallHierarchyOutgoingCallsParams) 102 | : AsyncLspResult = async { 103 | // TODO: There is no memthod of SymbolFinder which can find all outgoing calls of a specific symbol. 104 | // Then how can we implement it? Parsing AST manually? 105 | return None |> LspResult.success 106 | } 107 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/CodeLens.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis 6 | open Microsoft.CodeAnalysis.CSharp 7 | open Microsoft.CodeAnalysis.CSharp.Syntax 8 | open Microsoft.CodeAnalysis.Text 9 | open Ionide.LanguageServerProtocol.Server 10 | open Ionide.LanguageServerProtocol.Types 11 | open Ionide.LanguageServerProtocol.JsonRpc 12 | 13 | open CSharpLanguageServer.State 14 | open CSharpLanguageServer.Conversions 15 | open CSharpLanguageServer.Types 16 | 17 | type private DocumentSymbolCollectorForCodeLens(semanticModel: SemanticModel) = 18 | inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) 19 | 20 | let mutable collectedSymbols = [] 21 | 22 | let collect (node: SyntaxNode) (nameSpan: TextSpan) = 23 | let symbol = semanticModel.GetDeclaredSymbol(node) 24 | collectedSymbols <- (symbol, nameSpan) :: collectedSymbols 25 | 26 | member __.GetSymbols() = 27 | collectedSymbols |> List.rev |> Array.ofList 28 | 29 | override __.VisitEnumDeclaration(node) = 30 | collect node node.Identifier.Span 31 | base.VisitEnumDeclaration(node) 32 | 33 | override __.VisitEnumMemberDeclaration(node) = 34 | collect node node.Identifier.Span 35 | 36 | override __.VisitClassDeclaration(node) = 37 | collect node node.Identifier.Span 38 | base.VisitClassDeclaration(node) 39 | 40 | override __.VisitRecordDeclaration(node) = 41 | collect node node.Identifier.Span 42 | base.VisitRecordDeclaration(node) 43 | 44 | override __.VisitStructDeclaration(node) = 45 | collect node node.Identifier.Span 46 | base.VisitStructDeclaration(node) 47 | 48 | override __.VisitInterfaceDeclaration(node) = 49 | collect node node.Identifier.Span 50 | base.VisitInterfaceDeclaration(node) 51 | 52 | override __.VisitDelegateDeclaration(node) = 53 | collect node node.Identifier.Span 54 | 55 | override __.VisitConstructorDeclaration(node) = 56 | collect node node.Identifier.Span 57 | 58 | override __.VisitDestructorDeclaration(node) = 59 | collect node node.Identifier.Span 60 | 61 | override __.VisitOperatorDeclaration(node) = 62 | collect node node.OperatorToken.Span 63 | 64 | override __.VisitIndexerDeclaration(node) = 65 | collect node node.ThisKeyword.Span 66 | 67 | override __.VisitConversionOperatorDeclaration(node) = 68 | collect node node.Type.Span 69 | 70 | override __.VisitMethodDeclaration(node) = 71 | collect node node.Identifier.Span 72 | 73 | override __.VisitPropertyDeclaration(node) = 74 | collect node node.Identifier.Span 75 | 76 | override __.VisitVariableDeclarator(node) = 77 | let grandparent = 78 | node.Parent 79 | |> Option.ofObj 80 | |> Option.bind (fun node -> node.Parent |> Option.ofObj) 81 | // Only show field variables and ignore local variables 82 | if grandparent.IsSome && grandparent.Value :? FieldDeclarationSyntax then 83 | collect node node.Identifier.Span 84 | 85 | override __.VisitEventDeclaration(node) = 86 | collect node node.Identifier.Span 87 | 88 | [] 89 | module CodeLens = 90 | type CodeLensData = 91 | { DocumentUri: string 92 | Position: Position } 93 | static member Default = 94 | { DocumentUri = "" 95 | Position = { Line = 0u; Character = 0u } } 96 | 97 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 98 | clientCapabilities.TextDocument 99 | |> Option.bind (fun x -> x.CodeLens) 100 | |> Option.bind (fun x -> x.DynamicRegistration) 101 | |> Option.defaultValue false 102 | 103 | let provider (clientCapabilities: ClientCapabilities) : CodeLensOptions option = 104 | match dynamicRegistration clientCapabilities with 105 | | true -> None 106 | | false -> Some { ResolveProvider = Some true 107 | WorkDoneProgress = None } 108 | 109 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 110 | match dynamicRegistration clientCapabilities with 111 | | false -> None 112 | | true -> 113 | let registerOptions: CodeLensRegistrationOptions = 114 | { ResolveProvider = Some true 115 | WorkDoneProgress = None 116 | DocumentSelector = Some defaultDocumentSelector } 117 | 118 | Some 119 | { Id = Guid.NewGuid().ToString() 120 | Method = "textDocument/codeLens" 121 | RegisterOptions = registerOptions |> serialize |> Some } 122 | 123 | let handle (context: ServerRequestContext) (p: CodeLensParams): AsyncLspResult = async { 124 | let docMaybe = context.GetDocument p.TextDocument.Uri 125 | match docMaybe with 126 | | None -> 127 | return None |> LspResult.success 128 | | Some doc -> 129 | let! ct = Async.CancellationToken 130 | let! semanticModel = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask 131 | let! syntaxTree = doc.GetSyntaxTreeAsync(ct) |> Async.AwaitTask 132 | let! docText = doc.GetTextAsync(ct) |> Async.AwaitTask 133 | 134 | let collector = DocumentSymbolCollectorForCodeLens(semanticModel) 135 | let! root = syntaxTree.GetRootAsync(ct) |> Async.AwaitTask 136 | collector.Visit(root) 137 | 138 | let makeCodeLens (_symbol: ISymbol, nameSpan: TextSpan) : CodeLens = 139 | let start = nameSpan.Start |> docText.Lines.GetLinePosition 140 | 141 | let lensData: CodeLensData = 142 | { DocumentUri = p.TextDocument.Uri 143 | Position = start |> Position.fromLinePosition } 144 | 145 | { Range = nameSpan |> Range.fromTextSpan docText.Lines 146 | Command = None 147 | Data = lensData |> serialize |> Some } 148 | 149 | let codeLens = collector.GetSymbols() |> Seq.map makeCodeLens 150 | 151 | return codeLens |> Array.ofSeq |> Some |> LspResult.success 152 | } 153 | 154 | let resolve (context: ServerRequestContext) 155 | (p: CodeLens) 156 | : AsyncLspResult = async { 157 | let lensData = 158 | p.Data 159 | |> Option.map (fun t -> t.ToObject()) 160 | |> Option.defaultValue CodeLensData.Default 161 | 162 | match! context.FindSymbol lensData.DocumentUri lensData.Position with 163 | | None -> 164 | return p |> LspResult.success 165 | | Some symbol -> 166 | let! locations = context.FindReferences symbol false 167 | // FIXME: refNum is wrong. There are lots of false positive even if we distinct locations by 168 | // (l.SourceTree.FilePath, l.SourceSpan) 169 | let refNum = 170 | locations 171 | |> Seq.distinctBy (fun l -> (l.GetMappedLineSpan().Path, l.SourceSpan)) 172 | |> Seq.length 173 | 174 | let title = sprintf "%d Reference(s)" refNum 175 | 176 | let arg: ReferenceParams = 177 | { TextDocument = { Uri = lensData.DocumentUri } 178 | Position = lensData.Position 179 | WorkDoneToken = None 180 | PartialResultToken = None 181 | Context = { IncludeDeclaration = true } } 182 | let command = 183 | { Title = title 184 | Command = "textDocument/references" 185 | Arguments = Some [| arg |> serialize |] } 186 | 187 | return { p with Command = Some command } |> LspResult.success 188 | } 189 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Color.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module Color = 10 | let provider (clientCapabilities: ClientCapabilities) = None 11 | 12 | let registration (clientCapabilities: ClientCapabilities) : Registration option = None 13 | 14 | let handle (context: ServerRequestContext) (p: DocumentColorParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | 17 | let present (context: ServerRequestContext) (p: ColorPresentationParams) : AsyncLspResult = 18 | LspResult.notImplemented |> async.Return 19 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Completion.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | open System.Reflection 5 | 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | 10 | open CSharpLanguageServer 11 | open CSharpLanguageServer.State 12 | open CSharpLanguageServer.Util 13 | open CSharpLanguageServer.Conversions 14 | open CSharpLanguageServer.Types 15 | open CSharpLanguageServer.Logging 16 | 17 | [] 18 | module Completion = 19 | let private _logger = LogProvider.getLoggerByName "Completion" 20 | 21 | let emptyRoslynOptionSet: Microsoft.CodeAnalysis.Options.OptionSet = 22 | let osType = typeof 23 | let osEmptyOptionSetField = osType.GetField("Empty", BindingFlags.Static|||BindingFlags.NonPublic) 24 | osEmptyOptionSetField.GetValue(null) :?> Microsoft.CodeAnalysis.Options.OptionSet 25 | 26 | /// the type reflects on internal class Microsoft.CodeAnalysis.Completion.CompletionOptions 27 | /// see https://github.com/dotnet/roslyn/blob/main/src/Features/Core/Portable/Completion/CompletionOptions.cs 28 | type RoslynCompletionOptions = 29 | { 30 | Object: obj 31 | CompletionOptionsType: Type 32 | } 33 | with 34 | member rco.WithBool(optionName: string, optionValue: bool) = 35 | let cloneCompletionOptionsMI = rco.CompletionOptionsType.GetMethod("$") 36 | let updatedCompletionOptions = cloneCompletionOptionsMI.Invoke(rco.Object, null) 37 | let newCo = rco.CompletionOptionsType.GetProperty(optionName) 38 | newCo.SetValue(updatedCompletionOptions, optionValue) 39 | { rco with Object = updatedCompletionOptions } 40 | 41 | static member Default() = 42 | let featuresAssembly = Assembly.Load("Microsoft.CodeAnalysis.Features") 43 | let coType = featuresAssembly.GetType("Microsoft.CodeAnalysis.Completion.CompletionOptions") 44 | let defaultCo: obj = coType.GetField("Default").GetValue() 45 | { Object = defaultCo; CompletionOptionsType = coType } 46 | 47 | type RoslynCompletionServiceWrapper(service: Microsoft.CodeAnalysis.Completion.CompletionService) = 48 | member __.GetCompletionsAsync(doc, position, completionOptions, completionTrigger, ct) : Async = 49 | let completionServiceType = service.GetType() 50 | 51 | let getCompletionsAsync7MI = 52 | completionServiceType.GetMethods(BindingFlags.Instance|||BindingFlags.NonPublic) 53 | |> Seq.filter (fun mi -> mi.Name = "GetCompletionsAsync" && mi.GetParameters().Length = 7) 54 | |> Seq.head 55 | 56 | let parameters: obj array = [| doc; position; completionOptions.Object; emptyRoslynOptionSet; completionTrigger; null; ct |] 57 | 58 | let result = getCompletionsAsync7MI.Invoke(service, parameters) 59 | 60 | (result :?> System.Threading.Tasks.Task) 61 | |> Async.AwaitTask 62 | 63 | member __.ShouldTriggerCompletion(sourceText, position, completionTrigger) = 64 | service.ShouldTriggerCompletion(sourceText, position, completionTrigger) 65 | 66 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 67 | clientCapabilities.TextDocument 68 | |> Option.bind (fun x -> x.Completion) 69 | |> Option.bind (fun x -> x.DynamicRegistration) 70 | |> Option.defaultValue false 71 | 72 | let provider (clientCapabilities: ClientCapabilities) : CompletionOptions option = 73 | match dynamicRegistration clientCapabilities with 74 | | true -> None 75 | | false -> 76 | Some { ResolveProvider = None 77 | TriggerCharacters = Some ([| "."; "'"; |]) 78 | AllCommitCharacters = None 79 | WorkDoneProgress = None 80 | CompletionItem = None } 81 | 82 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 83 | match dynamicRegistration clientCapabilities with 84 | | false -> None 85 | | true -> 86 | let registerOptions: CompletionRegistrationOptions = 87 | { DocumentSelector = Some defaultDocumentSelector 88 | ResolveProvider = Some true 89 | TriggerCharacters = Some ([| "."; "'"; |]) 90 | AllCommitCharacters = None 91 | CompletionItem = None 92 | WorkDoneProgress = None 93 | } 94 | 95 | Some 96 | { Id = Guid.NewGuid().ToString() 97 | Method = "textDocument/completion" 98 | RegisterOptions = registerOptions |> serialize |> Some } 99 | 100 | let private roslynTagToLspCompletion tag = 101 | match tag with 102 | | "Class" -> CompletionItemKind.Class 103 | | "Delegate" -> CompletionItemKind.Function 104 | | "Enum" -> CompletionItemKind.Enum 105 | | "EnumMember" -> CompletionItemKind.EnumMember 106 | | "Interface" -> CompletionItemKind.Interface 107 | | "Struct" -> CompletionItemKind.Struct 108 | | "Local" -> CompletionItemKind.Variable 109 | | "Parameter" -> CompletionItemKind.Variable 110 | | "RangeVariable" -> CompletionItemKind.Variable 111 | | "Const" -> CompletionItemKind.Constant 112 | | "Event" -> CompletionItemKind.Event 113 | | "Field" -> CompletionItemKind.Field 114 | | "Method" -> CompletionItemKind.Method 115 | | "Property" -> CompletionItemKind.Property 116 | | "Label" -> CompletionItemKind.Unit 117 | | "Keyword" -> CompletionItemKind.Keyword 118 | | "Namespace" -> CompletionItemKind.Module 119 | | _ -> CompletionItemKind.Property 120 | 121 | // TODO: Add parameters to label so that we can distinguish override versions? 122 | // TODO: Change parameters to snippets like clangd 123 | let private makeLspCompletionItem 124 | (item: Microsoft.CodeAnalysis.Completion.CompletionItem) 125 | (cacheKey: uint64) = 126 | { Ionide.LanguageServerProtocol.Types.CompletionItem.Create(item.DisplayText) with 127 | Kind = item.Tags |> Seq.tryHead |> Option.map roslynTagToLspCompletion 128 | SortText = item.SortText |> Option.ofString 129 | FilterText = item.FilterText |> Option.ofString 130 | Detail = item.InlineDescription |> Option.ofString 131 | TextEditText = item.DisplayTextPrefix |> Option.ofObj 132 | InsertTextFormat = Some InsertTextFormat.PlainText 133 | Data = cacheKey |> serialize |> Some } 134 | 135 | let private cache = new LruCache<(Microsoft.CodeAnalysis.Document * Microsoft.CodeAnalysis.Completion.CompletionList)>(5) 136 | 137 | let handle (context: ServerRequestContext) (p: CompletionParams) : Async option>> = async { 138 | match context.GetDocument p.TextDocument.Uri with 139 | | None -> 140 | return None |> LspResult.success 141 | | Some doc -> 142 | let! ct = Async.CancellationToken 143 | let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask 144 | 145 | let position = Position.toRoslynPosition sourceText.Lines p.Position 146 | 147 | let completionService = 148 | Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc) 149 | |> RoslynCompletionServiceWrapper 150 | 151 | let completionOptions = 152 | RoslynCompletionOptions.Default() 153 | |> _.WithBool("ShowItemsFromUnimportedNamespaces", false) 154 | |> _.WithBool("ShowNameSuggestions", false) 155 | 156 | let completionTrigger = CompletionContext.toCompletionTrigger p.Context 157 | let shouldTriggerCompletion = 158 | p.Context |> Option.exists (fun x -> x.TriggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions) || 159 | completionService.ShouldTriggerCompletion(sourceText, position, completionTrigger) 160 | 161 | let! completions = 162 | if shouldTriggerCompletion then 163 | completionService.GetCompletionsAsync(doc, position, completionOptions, completionTrigger, ct) 164 | |> Async.map Option.ofObj 165 | else 166 | async.Return None 167 | 168 | return 169 | completions 170 | |> Option.map (fun completions -> 171 | let key = cache.add((doc, completions)) 172 | let items = 173 | completions.ItemsList 174 | |> Seq.map (flip makeLspCompletionItem key) 175 | |> Array.ofSeq 176 | { IsIncomplete = true 177 | Items = items 178 | ItemDefaults = None }) 179 | |> Option.map U2.C2 180 | |> LspResult.success 181 | } 182 | 183 | let resolve (_context: ServerRequestContext) (item: CompletionItem) : AsyncLspResult = async { 184 | match 185 | item.Data 186 | |> Option.bind deserialize 187 | |> Option.bind cache.get 188 | |> Option.bind (fun (doc, cachedItems) -> 189 | cachedItems.ItemsList 190 | |> Seq.tryFind (fun x -> x.DisplayText = item.Label && (item.SortText.IsNone || x.SortText = item.SortText.Value)) 191 | |> Option.map (fun x -> (doc, x))) 192 | with 193 | | Some (doc, cachedItem) -> 194 | let completionService = Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc) 195 | let! ct = Async.CancellationToken 196 | let! description = 197 | completionService.GetDescriptionAsync(doc, cachedItem, ct) 198 | |> Async.AwaitTask 199 | |> Async.map Option.ofObj 200 | // TODO: make the doc as a markdown string instead of a plain text 201 | let itemDocumentation = description |> Option.map Documentation.fromCompletionDescription 202 | return { item with Documentation = itemDocumentation |> Option.map U2.C2 } 203 | |> LspResult.success 204 | | None -> 205 | return item |> LspResult.success 206 | } 207 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Declaration.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module Declaration = 10 | let provider (_cc: ClientCapabilities) : bool option = None 11 | 12 | let registration (_cc: ClientCapabilities) : Registration option = None 13 | 14 | let handle (_context: ServerRequestContext) (_p: DeclarationParams) : AsyncLspResult option> = 15 | LspResult.notImplemented option> |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Definition.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Ionide.LanguageServerProtocol.Server 6 | open Ionide.LanguageServerProtocol.Types 7 | open Ionide.LanguageServerProtocol.JsonRpc 8 | 9 | open CSharpLanguageServer.State 10 | open CSharpLanguageServer.Types 11 | 12 | [] 13 | module Definition = 14 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 15 | clientCapabilities.TextDocument 16 | |> Option.bind (fun x -> x.Definition) 17 | |> Option.bind (fun x -> x.DynamicRegistration) 18 | |> Option.defaultValue false 19 | 20 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 21 | match dynamicRegistration clientCapabilities with 22 | | true -> None 23 | | false -> Some (U2.C1 true) 24 | 25 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 26 | match dynamicRegistration clientCapabilities with 27 | | false -> None 28 | | true -> 29 | let registerOptions: DefinitionRegistrationOptions = 30 | { DocumentSelector = Some defaultDocumentSelector 31 | WorkDoneProgress = None } 32 | Some 33 | { Id = Guid.NewGuid().ToString() 34 | Method = "textDocument/definition" 35 | RegisterOptions = registerOptions |> serialize |> Some } 36 | 37 | let handle (context: ServerRequestContext) (p: DefinitionParams) : Async option>> = async { 38 | match! context.FindSymbol' p.TextDocument.Uri p.Position with 39 | | None -> 40 | return None |> LspResult.success 41 | | Some (symbol, doc) -> 42 | let! locations = context.ResolveSymbolLocations symbol (Some doc.Project) 43 | return 44 | locations 45 | |> Array.ofList 46 | |> Definition.C2 47 | |> U2.C1 48 | |> Some 49 | |> LspResult.success 50 | } 51 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Diagnostic.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Ionide.LanguageServerProtocol.Server 6 | open Ionide.LanguageServerProtocol.Types 7 | open Ionide.LanguageServerProtocol.JsonRpc 8 | 9 | open CSharpLanguageServer.Conversions 10 | open CSharpLanguageServer.State 11 | open CSharpLanguageServer.Types 12 | open CSharpLanguageServer.Logging 13 | 14 | [] 15 | module Diagnostic = 16 | let private logger = LogProvider.getLoggerByName "Diagnostic" 17 | 18 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 19 | clientCapabilities.TextDocument 20 | |> Option.bind (fun x -> x.Diagnostic) 21 | |> Option.bind (fun x -> x.DynamicRegistration) 22 | |> Option.defaultValue false 23 | 24 | let provider (clientCapabilities: ClientCapabilities): U2 option = 25 | match dynamicRegistration clientCapabilities with 26 | | true -> None 27 | | false -> 28 | let diagnosticOptions: DiagnosticRegistrationOptions = 29 | { DocumentSelector = Some defaultDocumentSelector 30 | WorkDoneProgress = None 31 | Identifier = None 32 | InterFileDependencies = false 33 | WorkspaceDiagnostics = false 34 | Id = None 35 | } 36 | 37 | Some (U2.C2 diagnosticOptions) 38 | 39 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 40 | match dynamicRegistration clientCapabilities with 41 | | false -> None 42 | | true -> 43 | let registerOptions: DiagnosticRegistrationOptions = 44 | { DocumentSelector = Some defaultDocumentSelector 45 | WorkDoneProgress = None 46 | Identifier = None 47 | InterFileDependencies = false 48 | WorkspaceDiagnostics = false 49 | Id = None } 50 | 51 | Some 52 | { Id = Guid.NewGuid().ToString() 53 | Method = "textDocument/diagnostic" 54 | RegisterOptions = registerOptions |> serialize |> Some } 55 | 56 | let handle (context: ServerRequestContext) (p: DocumentDiagnosticParams) : AsyncLspResult = async { 57 | let emptyReport: RelatedFullDocumentDiagnosticReport = 58 | { 59 | Kind = "full" 60 | ResultId = None 61 | Items = [| |] 62 | RelatedDocuments = None 63 | } 64 | 65 | match context.GetDocument p.TextDocument.Uri with 66 | | None -> 67 | return emptyReport |> U2.C1 |> LspResult.success 68 | 69 | | Some doc -> 70 | let! ct = Async.CancellationToken 71 | let! semanticModelMaybe = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask 72 | match semanticModelMaybe |> Option.ofObj with 73 | | Some semanticModel -> 74 | let diagnostics = 75 | semanticModel.GetDiagnostics() 76 | |> Seq.map Diagnostic.fromRoslynDiagnostic 77 | |> Array.ofSeq 78 | 79 | return { emptyReport with Items = diagnostics } 80 | |> U2.C1 81 | |> LspResult.success 82 | 83 | | None -> 84 | return emptyReport |> U2.C1 |> LspResult.success 85 | } 86 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/DocumentFormatting.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis.Formatting 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | 10 | open CSharpLanguageServer 11 | open CSharpLanguageServer.State 12 | open CSharpLanguageServer.Types 13 | 14 | [] 15 | module DocumentFormatting = 16 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 17 | clientCapabilities.TextDocument 18 | |> Option.bind (fun x -> x.Formatting) 19 | |> Option.bind (fun x -> x.DynamicRegistration) 20 | |> Option.defaultValue false 21 | 22 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 23 | match dynamicRegistration clientCapabilities with 24 | | true -> None 25 | | false -> Some (U2.C1 true) 26 | 27 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 28 | match dynamicRegistration clientCapabilities with 29 | | false -> None 30 | | true -> 31 | let registerOptions: DocumentFormattingRegistrationOptions = 32 | { DocumentSelector = Some defaultDocumentSelector 33 | WorkDoneProgress = None } 34 | Some 35 | { Id = Guid.NewGuid().ToString() 36 | Method = "textDocument/formatting" 37 | RegisterOptions = registerOptions |> serialize |> Some } 38 | 39 | let handle (context: ServerRequestContext) (p: DocumentFormattingParams) : AsyncLspResult = async { 40 | match context.GetUserDocument p.TextDocument.Uri with 41 | | None -> return None |> LspResult.success 42 | | Some doc -> 43 | let! ct = Async.CancellationToken 44 | let! options = FormatUtil.getFormattingOptions context.State.Settings doc p.Options 45 | let! newDoc = Formatter.FormatAsync(doc, options, cancellationToken=ct) |> Async.AwaitTask 46 | let! textEdits = FormatUtil.getChanges newDoc doc 47 | return textEdits |> Some |> LspResult.success 48 | } 49 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/DocumentHighlight.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | open System.Collections.Immutable 5 | 6 | open Microsoft.CodeAnalysis 7 | open Microsoft.CodeAnalysis.FindSymbols 8 | open Ionide.LanguageServerProtocol.Server 9 | open Ionide.LanguageServerProtocol.Types 10 | open Ionide.LanguageServerProtocol.JsonRpc 11 | 12 | open CSharpLanguageServer.Types 13 | open CSharpLanguageServer.State 14 | open CSharpLanguageServer.Conversions 15 | 16 | [] 17 | module DocumentHighlight = 18 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 19 | clientCapabilities.TextDocument 20 | |> Option.bind (fun x -> x.DocumentHighlight) 21 | |> Option.bind (fun x -> x.DynamicRegistration) 22 | |> Option.defaultValue false 23 | 24 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 25 | match dynamicRegistration clientCapabilities with 26 | | true -> None 27 | | false -> Some (U2.C1 true) 28 | 29 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 30 | match dynamicRegistration clientCapabilities with 31 | | false -> None 32 | | true -> 33 | let registerOptions: DocumentHighlightRegistrationOptions = 34 | { DocumentSelector = Some defaultDocumentSelector 35 | WorkDoneProgress = None 36 | } 37 | 38 | Some 39 | { Id = Guid.NewGuid().ToString() 40 | Method = "textDocument/documentHighlight" 41 | RegisterOptions = registerOptions |> serialize |> Some } 42 | 43 | let private shouldHighlight (symbol: ISymbol) = 44 | match symbol with 45 | | :? INamespaceSymbol -> false 46 | | _ -> true 47 | 48 | let handle (context: ServerRequestContext) (p: DocumentHighlightParams) : AsyncLspResult = async { 49 | let! ct = Async.CancellationToken 50 | let filePath = Uri.toPath p.TextDocument.Uri 51 | 52 | // We only need to find references in the file (not the whole workspace), so we don't use 53 | // context.FindSymbol & context.FindReferences here. 54 | let getHighlights (symbol: ISymbol) (doc: Document) = async { 55 | let docSet = ImmutableHashSet.Create(doc) 56 | let! refs = SymbolFinder.FindReferencesAsync(symbol, doc.Project.Solution, docSet, cancellationToken=ct) |> Async.AwaitTask 57 | let! def = SymbolFinder.FindSourceDefinitionAsync(symbol, doc.Project.Solution, cancellationToken=ct) |> Async.AwaitTask 58 | 59 | let locations = 60 | refs 61 | |> Seq.collect (fun r -> r.Locations) 62 | |> Seq.map (fun rl -> rl.Location) 63 | |> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath) 64 | |> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations)) 65 | 66 | return 67 | locations 68 | |> Seq.map Location.fromRoslynLocation 69 | |> Seq.filter _.IsSome 70 | |> Seq.map _.Value 71 | |> Seq.map (fun l -> 72 | { Range = l.Range 73 | Kind = Some DocumentHighlightKind.Read }) 74 | } 75 | 76 | match! context.FindSymbol' p.TextDocument.Uri p.Position with 77 | | None -> return None |> LspResult.success 78 | | Some (symbol, doc) -> 79 | match Option.ofObj symbol with 80 | | Some symbol when shouldHighlight symbol -> 81 | let! highlights = getHighlights symbol doc 82 | return highlights |> Seq.toArray |> Some |> LspResult.success 83 | | _ -> return None |> LspResult.success 84 | } 85 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/DocumentLink.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module DocumentLink = 10 | let provider (clientCapabilities: ClientCapabilities) : DocumentLinkOptions option = None 11 | 12 | let registration (clientCapabilities: ClientCapabilities) : Registration option = None 13 | 14 | let handle (context: ServerRequestContext) (p: DocumentLinkParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | 17 | let resolve (context: ServerRequestContext) (p: DocumentLink) : AsyncLspResult = 18 | LspResult.notImplemented |> async.Return 19 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis.Text 6 | open Microsoft.CodeAnalysis.Formatting 7 | open Ionide.LanguageServerProtocol.Server 8 | open Ionide.LanguageServerProtocol.Types 9 | open Ionide.LanguageServerProtocol.JsonRpc 10 | 11 | open CSharpLanguageServer 12 | open CSharpLanguageServer.State 13 | open CSharpLanguageServer.Types 14 | open CSharpLanguageServer.Conversions 15 | 16 | [] 17 | module DocumentOnTypeFormatting = 18 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 19 | clientCapabilities.TextDocument 20 | |> Option.bind (fun x -> x.OnTypeFormatting) 21 | |> Option.bind (fun x -> x.DynamicRegistration) 22 | |> Option.defaultValue false 23 | 24 | let provider (clientCapabilities: ClientCapabilities) : DocumentOnTypeFormattingOptions option = 25 | match dynamicRegistration clientCapabilities with 26 | | true -> None 27 | | false -> 28 | Some 29 | { FirstTriggerCharacter = ";" 30 | MoreTriggerCharacter = Some([| "}"; ")" |]) } 31 | 32 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 33 | match dynamicRegistration clientCapabilities with 34 | | false -> None 35 | | true -> 36 | let registerOptions: DocumentOnTypeFormattingRegistrationOptions = 37 | { FirstTriggerCharacter = ";" 38 | MoreTriggerCharacter = Some([| "}"; ")" |]) 39 | DocumentSelector = Some defaultDocumentSelector 40 | } 41 | Some 42 | { Id = Guid.NewGuid().ToString() 43 | Method = "textDocument/onTypeFormatting" 44 | RegisterOptions = registerOptions |> serialize |> Some } 45 | 46 | let handle (context: ServerRequestContext) (p: DocumentOnTypeFormattingParams) : AsyncLspResult = async { 47 | match context.GetUserDocument p.TextDocument.Uri with 48 | | None -> 49 | return None |> LspResult.success 50 | | Some doc -> 51 | let! options = FormatUtil.getFormattingOptions context.State.Settings doc p.Options 52 | let! ct = Async.CancellationToken 53 | let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask 54 | let pos = Position.toRoslynPosition sourceText.Lines p.Position 55 | 56 | match p.Ch with 57 | | ";" 58 | | "}" 59 | | ")" -> 60 | let! root = doc.GetSyntaxRootAsync(ct) |> Async.AwaitTask 61 | match FormatUtil.findFormatTarget root pos with 62 | | None -> return None |> LspResult.success 63 | | Some node -> 64 | let! newDoc = 65 | Formatter.FormatAsync(doc, TextSpan.FromBounds(node.FullSpan.Start, node.FullSpan.End), options, cancellationToken=ct) 66 | |> Async.AwaitTask 67 | let! textEdits = FormatUtil.getChanges newDoc doc 68 | return textEdits |> Some |> LspResult.success 69 | | _ -> 70 | return None |> LspResult.success 71 | } 72 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis.Formatting 6 | open Microsoft.CodeAnalysis.Text 7 | open Ionide.LanguageServerProtocol.Server 8 | open Ionide.LanguageServerProtocol.Types 9 | open Ionide.LanguageServerProtocol.JsonRpc 10 | 11 | open CSharpLanguageServer 12 | open CSharpLanguageServer.State 13 | open CSharpLanguageServer.Conversions 14 | open CSharpLanguageServer.Types 15 | 16 | [] 17 | module DocumentRangeFormatting = 18 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 19 | clientCapabilities.TextDocument 20 | |> Option.bind (fun x -> x.RangeFormatting) 21 | |> Option.bind (fun x -> x.DynamicRegistration) 22 | |> Option.defaultValue false 23 | 24 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 25 | match dynamicRegistration clientCapabilities with 26 | | true -> None 27 | | false -> Some (U2.C1 true) 28 | 29 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 30 | match dynamicRegistration clientCapabilities with 31 | | false -> None 32 | | true -> 33 | let registerOptions: DocumentRangeFormattingRegistrationOptions = 34 | { DocumentSelector = Some defaultDocumentSelector 35 | WorkDoneProgress = None 36 | } 37 | 38 | Some 39 | { Id = Guid.NewGuid().ToString() 40 | Method = "textDocument/rangeFormatting" 41 | RegisterOptions = registerOptions |> serialize |> Some } 42 | 43 | let handle (context: ServerRequestContext) (p: DocumentRangeFormattingParams) : AsyncLspResult = async { 44 | match context.GetUserDocument p.TextDocument.Uri with 45 | | None -> 46 | return None |> LspResult.success 47 | | Some doc -> 48 | let! ct = Async.CancellationToken 49 | let! options = FormatUtil.getFormattingOptions context.State.Settings doc p.Options 50 | let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask 51 | let startPos = Position.toRoslynPosition sourceText.Lines p.Range.Start 52 | let endPos = Position.toRoslynPosition sourceText.Lines p.Range.End 53 | let! syntaxTree = doc.GetSyntaxRootAsync(ct) |> Async.AwaitTask 54 | let tokenStart = syntaxTree.FindToken(startPos).FullSpan.Start 55 | let! newDoc = 56 | Formatter.FormatAsync(doc, TextSpan.FromBounds(tokenStart, endPos), options, cancellationToken=ct) 57 | |> Async.AwaitTask 58 | let! textEdits = FormatUtil.getChanges newDoc doc 59 | return textEdits |> Some |> LspResult.success 60 | } 61 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/ExecuteCommand.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module ExecuteCommand = 10 | let provider (_cc: ClientCapabilities) : ExecuteCommandOptions option = None 11 | 12 | let registration (_cc: ClientCapabilities) : Registration option = None 13 | 14 | let handle (_context: ServerRequestContext) (_p: ExecuteCommandParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/FoldingRange.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module FoldingRange = 10 | let provider (_c: ClientCapabilities) : bool option = None 11 | 12 | let registration (_c: ClientCapabilities) : Registration option = None 13 | 14 | let handle (_c: ServerRequestContext) (_p: FoldingRangeParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Hover.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Ionide.LanguageServerProtocol.Server 6 | open Ionide.LanguageServerProtocol.Types 7 | open Ionide.LanguageServerProtocol.JsonRpc 8 | 9 | open CSharpLanguageServer 10 | open CSharpLanguageServer.Types 11 | open CSharpLanguageServer.State 12 | 13 | [] 14 | module Hover = 15 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 16 | clientCapabilities.TextDocument 17 | |> Option.bind (fun x -> x.Hover) 18 | |> Option.bind (fun x -> x.DynamicRegistration) 19 | |> Option.defaultValue false 20 | 21 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 22 | match dynamicRegistration clientCapabilities with 23 | | true -> Some (U2.C1 false) 24 | | false -> Some (U2.C1 true) 25 | 26 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 27 | match dynamicRegistration clientCapabilities with 28 | | false -> None 29 | | true -> 30 | let registerOptions: HoverRegistrationOptions = 31 | { DocumentSelector = Some defaultDocumentSelector 32 | WorkDoneProgress = None 33 | } 34 | Some 35 | { Id = Guid.NewGuid().ToString() 36 | Method = "textDocument/hover" 37 | RegisterOptions = registerOptions |> serialize |> Some } 38 | 39 | let handle (context: ServerRequestContext) (p: HoverParams) : AsyncLspResult = async { 40 | match! context.FindSymbol' p.TextDocument.Uri p.Position with 41 | | None -> 42 | return None |> LspResult.success 43 | | Some (symbol, doc) -> 44 | let content = DocumentationUtil.markdownDocForSymbolWithSignature symbol 45 | let hover = 46 | { Contents = { Kind = MarkupKind.Markdown; Value = content } |> U3.C1 47 | // TODO: Support range 48 | Range = None } 49 | return hover |> Some |> LspResult.success 50 | } 51 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Implementation.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Ionide.LanguageServerProtocol.Server 6 | open Ionide.LanguageServerProtocol.Types 7 | open Ionide.LanguageServerProtocol.JsonRpc 8 | 9 | open CSharpLanguageServer.Types 10 | open CSharpLanguageServer.State 11 | open CSharpLanguageServer.Util 12 | 13 | [] 14 | module Implementation = 15 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 16 | clientCapabilities.TextDocument 17 | |> Option.bind (fun x -> x.Implementation) 18 | |> Option.bind (fun x -> x.DynamicRegistration) 19 | |> Option.defaultValue false 20 | 21 | let provider (clientCapabilities: ClientCapabilities) : U3 option = 22 | match dynamicRegistration clientCapabilities with 23 | | true -> None 24 | | false -> Some (U3.C1 true) 25 | 26 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 27 | match dynamicRegistration clientCapabilities with 28 | | false -> None 29 | | true -> 30 | let registerOptions: ImplementationRegistrationOptions = 31 | { DocumentSelector = Some defaultDocumentSelector 32 | Id = None 33 | WorkDoneProgress = None } 34 | 35 | Some 36 | { Id = Guid.NewGuid().ToString() 37 | Method = "textDocument/implementation" 38 | RegisterOptions = registerOptions |> serialize |> Some } 39 | 40 | let handle (context: ServerRequestContext) (p: ImplementationParams) : Async option>> = async { 41 | match! context.FindSymbol p.TextDocument.Uri p.Position with 42 | | None -> return None |> LspResult.success 43 | | Some symbol -> 44 | let! impls = context.FindImplementations symbol 45 | let! locations = impls |> Seq.map (flip context.ResolveSymbolLocations None) |> Async.Parallel 46 | 47 | return 48 | locations 49 | |> Array.collect List.toArray 50 | |> Declaration.C2 51 | |> U2.C1 52 | |> Some 53 | |> LspResult.success 54 | } 55 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Initialization.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | open System.IO 5 | open System.Reflection 6 | 7 | open Microsoft.Build.Locator 8 | open Ionide.LanguageServerProtocol 9 | open Ionide.LanguageServerProtocol.Types 10 | open Ionide.LanguageServerProtocol.Server 11 | open Ionide.LanguageServerProtocol.JsonRpc 12 | 13 | open CSharpLanguageServer.State 14 | open CSharpLanguageServer.State.ServerState 15 | open CSharpLanguageServer.Types 16 | open CSharpLanguageServer.Logging 17 | 18 | [] 19 | module Initialization = 20 | let private logger = LogProvider.getLoggerByName "Initialization" 21 | 22 | let handleInitialize (lspClient: ILspClient) 23 | (setupTimer: unit -> unit) 24 | (serverCapabilities: ServerCapabilities) 25 | (context: ServerRequestContext) 26 | (p: InitializeParams) 27 | : Async> = async { 28 | // context.State.LspClient has not been initialized yet thus context.WindowShowMessage will not work 29 | let windowShowMessage m = lspClient.WindowLogMessage({ Type = MessageType.Info; Message = m }) 30 | 31 | context.Emit(ClientChange (Some lspClient)) 32 | 33 | let serverName = "csharp-ls" 34 | let serverVersion = Assembly.GetExecutingAssembly().GetName().Version |> string 35 | logger.info ( 36 | Log.setMessage "initializing, {name} version {version}" 37 | >> Log.addContext "name" serverName 38 | >> Log.addContext "version" serverVersion 39 | ) 40 | 41 | do! windowShowMessage( 42 | sprintf "csharp-ls: initializing, version %s" serverVersion) 43 | 44 | logger.info ( 45 | Log.setMessage "{name} is released under MIT license and is not affiliated with Microsoft Corp.; see https://github.com/razzmatazz/csharp-language-server" 46 | >> Log.addContext "name" serverName 47 | ) 48 | 49 | do! windowShowMessage( 50 | sprintf "csharp-ls: %s is released under MIT license and is not affiliated with Microsoft Corp.; see https://github.com/razzmatazz/csharp-language-server" serverName) 51 | 52 | let vsInstanceQueryOpt = VisualStudioInstanceQueryOptions.Default 53 | let vsInstanceList = MSBuildLocator.QueryVisualStudioInstances(vsInstanceQueryOpt) 54 | if Seq.isEmpty vsInstanceList then 55 | raise (InvalidOperationException("No instances of MSBuild could be detected." + Environment.NewLine + "Try calling RegisterInstance or RegisterMSBuildPath to manually register one.")) 56 | 57 | // do! infoMessage "MSBuildLocator instances found:" 58 | // 59 | // for vsInstance in vsInstanceList do 60 | // do! infoMessage (sprintf "- SDK=\"%s\", Version=%s, MSBuildPath=\"%s\", DiscoveryType=%s" 61 | // vsInstance.Name 62 | // (string vsInstance.Version) 63 | // vsInstance.MSBuildPath 64 | // (string vsInstance.DiscoveryType)) 65 | 66 | let vsInstance = vsInstanceList |> Seq.head 67 | 68 | logger.info( 69 | Log.setMessage "MSBuildLocator: will register \"{vsInstanceName}\", Version={vsInstanceVersion} as default instance" 70 | >> Log.addContext "vsInstanceName" vsInstance.Name 71 | >> Log.addContext "vsInstanceVersion" (string vsInstance.Version) 72 | ) 73 | 74 | MSBuildLocator.RegisterInstance(vsInstance) 75 | 76 | (* 77 | logger.trace ( 78 | Log.setMessage "handleInitialize: p.Capabilities={caps}" 79 | >> Log.addContext "caps" (serialize p.Capabilities) 80 | ) 81 | *) 82 | context.Emit(ClientCapabilityChange p.Capabilities) 83 | 84 | // TODO use p.RootUri 85 | let rootPath = Directory.GetCurrentDirectory() 86 | context.Emit(RootPathChange rootPath) 87 | 88 | // setup timer so actors get period ticks 89 | setupTimer() 90 | 91 | let initializeResult = 92 | { InitializeResult.Default with 93 | Capabilities = serverCapabilities 94 | ServerInfo = 95 | Some 96 | { Name = "csharp-ls" 97 | Version = Some (Assembly.GetExecutingAssembly().GetName().Version.ToString()) }} 98 | 99 | return initializeResult |> LspResult.success 100 | } 101 | 102 | let handleInitialized (lspClient: ILspClient) 103 | (stateActor: MailboxProcessor) 104 | (getRegistrations: ClientCapabilities -> Registration list) 105 | (context: ServerRequestContext) 106 | (_p: unit) 107 | : Async> = 108 | async { 109 | logger.trace ( 110 | Log.setMessage "handleInitialized: \"initialized\" notification received from client" 111 | ) 112 | 113 | let registrationParams = { Registrations = getRegistrations context.ClientCapabilities |> List.toArray } 114 | 115 | // TODO: Retry on error? 116 | try 117 | match! lspClient.ClientRegisterCapability registrationParams with 118 | | Ok _ -> () 119 | | Error error -> 120 | logger.warn( 121 | Log.setMessage "handleInitialized: didChangeWatchedFiles registration has failed with {error}" 122 | >> Log.addContext "error" (string error) 123 | ) 124 | with 125 | | ex -> 126 | logger.warn( 127 | Log.setMessage "handleInitialized: didChangeWatchedFiles registration has failed with {error}" 128 | >> Log.addContext "error" (string ex) 129 | ) 130 | 131 | // 132 | // retrieve csharp settings 133 | // 134 | try 135 | let! workspaceCSharpConfig = 136 | lspClient.WorkspaceConfiguration( 137 | { Items=[| { Section=Some "csharp"; ScopeUri=None } |] }) 138 | 139 | let csharpConfigTokensMaybe = 140 | match workspaceCSharpConfig with 141 | | Ok ts -> Some ts 142 | | _ -> None 143 | 144 | let newSettingsMaybe = 145 | match csharpConfigTokensMaybe with 146 | | Some [| t |] -> 147 | let csharpSettingsMaybe = t |> deserialize 148 | 149 | match csharpSettingsMaybe with 150 | | Some csharpSettings -> 151 | 152 | match csharpSettings.solution with 153 | | Some solutionPath-> Some { context.State.Settings with SolutionPath = Some solutionPath } 154 | | _ -> None 155 | 156 | | _ -> None 157 | | _ -> None 158 | 159 | // do! logMessage (sprintf "handleInitialized: newSettingsMaybe=%s" (string newSettingsMaybe)) 160 | 161 | match newSettingsMaybe with 162 | | Some newSettings -> 163 | context.Emit(SettingsChange newSettings) 164 | | _ -> () 165 | with 166 | | ex -> 167 | logger.warn( 168 | Log.setMessage "handleInitialized: could not retrieve `csharp` workspace configuration section: {error}" 169 | >> Log.addContext "error" (ex |> string) 170 | ) 171 | 172 | // 173 | // start loading the solution 174 | // 175 | stateActor.Post(SolutionReloadRequest (TimeSpan.FromMilliseconds(100))) 176 | 177 | logger.trace( 178 | Log.setMessage "handleInitialized: OK") 179 | 180 | return Ok() 181 | } 182 | 183 | let handleShutdown (context: ServerRequestContext) (_: unit) : Async> = async { 184 | context.Emit(ClientCapabilityChange emptyClientCapabilities) 185 | context.Emit(ClientChange None) 186 | return Ok() 187 | } 188 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/InlineValue.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module InlineValue = 10 | let provider (clientCapabilities: ClientCapabilities) : InlineValueOptions option = None 11 | 12 | let registration (clientCapabilities: ClientCapabilities) : Registration option = None 13 | 14 | let handle (context: ServerRequestContext) (p: InlineValueParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/LinkedEditingRange.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module LinkedEditingRange = 10 | let provider (clientCapabilities: ClientCapabilities) = None 11 | 12 | let registration (clientCapabilities: ClientCapabilities) : Registration option = None 13 | 14 | let handle (context: ServerRequestContext) (def: LinkedEditingRangeParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Moniker.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module Moniker = 10 | let provider (_cc: ClientCapabilities) = None 11 | 12 | let registration (_cc: ClientCapabilities) : Registration option = None 13 | 14 | let handle (_context: ServerRequestContext) (_p: MonikerParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/References.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Ionide.LanguageServerProtocol.Server 6 | open Ionide.LanguageServerProtocol.Types 7 | open Ionide.LanguageServerProtocol.JsonRpc 8 | 9 | open CSharpLanguageServer.State 10 | open CSharpLanguageServer.Conversions 11 | open CSharpLanguageServer.Types 12 | 13 | [] 14 | module References = 15 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 16 | clientCapabilities.TextDocument 17 | |> Option.bind (fun x -> x.References) 18 | |> Option.bind (fun x -> x.DynamicRegistration) 19 | |> Option.defaultValue false 20 | 21 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 22 | match dynamicRegistration clientCapabilities with 23 | | true -> None 24 | | false -> Some (U2.C1 true) 25 | 26 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 27 | match dynamicRegistration clientCapabilities with 28 | | false -> None 29 | | true -> 30 | let registerOptions: ReferenceRegistrationOptions = 31 | { 32 | DocumentSelector = Some defaultDocumentSelector 33 | WorkDoneProgress = None 34 | } 35 | 36 | Some 37 | { Id = Guid.NewGuid().ToString() 38 | Method = "textDocument/references" 39 | RegisterOptions = registerOptions |> serialize |> Some } 40 | 41 | let handle (context: ServerRequestContext) (p: ReferenceParams) : AsyncLspResult = async { 42 | match! context.FindSymbol p.TextDocument.Uri p.Position with 43 | | None -> return None |> LspResult.success 44 | | Some symbol -> 45 | let! locations = context.FindReferences symbol p.Context.IncludeDeclaration 46 | 47 | return 48 | locations 49 | |> Seq.map Location.fromRoslynLocation 50 | |> Seq.filter _.IsSome 51 | |> Seq.map _.Value 52 | |> Seq.distinct 53 | |> Seq.toArray 54 | |> Some 55 | |> LspResult.success 56 | } 57 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Rename.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | open System.Threading 5 | 6 | open Microsoft.CodeAnalysis 7 | open Microsoft.CodeAnalysis.CSharp.Syntax 8 | open Microsoft.CodeAnalysis.FindSymbols 9 | open Microsoft.CodeAnalysis.Rename 10 | open Ionide.LanguageServerProtocol.Server 11 | open Ionide.LanguageServerProtocol.Types 12 | open Ionide.LanguageServerProtocol.JsonRpc 13 | 14 | open CSharpLanguageServer.State 15 | open CSharpLanguageServer.Logging 16 | open CSharpLanguageServer.Conversions 17 | open CSharpLanguageServer.Types 18 | open CSharpLanguageServer.Util 19 | 20 | [] 21 | module Rename = 22 | let private logger = LogProvider.getLoggerByName "Rename" 23 | 24 | let private lspDocChangesFromSolutionDiff 25 | (ct: CancellationToken) 26 | (originalSolution: Solution) 27 | (updatedSolution: Solution) 28 | (tryGetDocVersionByUri: string -> int option) 29 | : Async = 30 | let getEdits 31 | (originalSolution: Solution) 32 | (updatedSolution: Solution) 33 | (docId: DocumentId) 34 | : Async = async { 35 | let originalDoc = originalSolution.GetDocument(docId) 36 | let! originalDocText = originalDoc.GetTextAsync(ct) |> Async.AwaitTask 37 | let updatedDoc = updatedSolution.GetDocument(docId) 38 | let! docChanges = updatedDoc.GetTextChangesAsync(originalDoc, ct) |> Async.AwaitTask 39 | 40 | let diffEdits: U2 array = 41 | docChanges 42 | |> Seq.sortBy (fun c -> c.Span.Start) 43 | |> Seq.map (TextEdit.fromTextChange originalDocText.Lines) 44 | |> Seq.map U2.C1 45 | |> Array.ofSeq 46 | 47 | let uri = originalDoc.FilePath |> Path.toUri 48 | let textEditDocument = 49 | { Uri = uri 50 | Version = tryGetDocVersionByUri uri } 51 | 52 | return 53 | { TextDocument = textEditDocument 54 | Edits = diffEdits } 55 | } 56 | 57 | updatedSolution.GetChanges(originalSolution).GetProjectChanges() 58 | |> Seq.collect (fun projectChange -> projectChange.GetChangedDocuments()) 59 | |> Seq.map (getEdits originalSolution updatedSolution) 60 | |> Async.Parallel 61 | |> Async.map (Seq.distinct >> Array.ofSeq) 62 | 63 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 64 | clientCapabilities.TextDocument 65 | |> Option.bind (fun x -> x.Rename) 66 | |> Option.bind (fun x -> x.DynamicRegistration) 67 | |> Option.defaultValue false 68 | 69 | let private prepareSupport (clientCapabilities: ClientCapabilities) = 70 | clientCapabilities.TextDocument 71 | |> Option.bind (fun x -> x.Rename) 72 | |> Option.bind (fun x -> x.PrepareSupport) 73 | |> Option.defaultValue false 74 | 75 | let provider (clientCapabilities: ClientCapabilities): U2 option = 76 | match dynamicRegistration clientCapabilities, prepareSupport clientCapabilities with 77 | | true, _ -> None 78 | | false, true -> Some (U2.C2 { PrepareProvider = Some true; WorkDoneProgress = None }) 79 | | false, false -> Some (U2.C1 true) 80 | 81 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 82 | match dynamicRegistration clientCapabilities with 83 | | false -> None 84 | | true -> 85 | let registerOptions: RenameRegistrationOptions = { 86 | PrepareProvider = Some (prepareSupport clientCapabilities) 87 | DocumentSelector = Some defaultDocumentSelector 88 | WorkDoneProgress = None 89 | } 90 | Some 91 | { Id = Guid.NewGuid().ToString() 92 | Method = "textDocument/rename" 93 | RegisterOptions = registerOptions |> serialize |> Some } 94 | 95 | let prepare (context: ServerRequestContext) 96 | (p: PrepareRenameParams) 97 | : AsyncLspResult = async { 98 | match context.GetUserDocument p.TextDocument.Uri with 99 | | None -> 100 | return None |> LspResult.success 101 | | Some doc -> 102 | let! ct = Async.CancellationToken 103 | let! docSyntaxTree = doc.GetSyntaxTreeAsync(ct) |> Async.AwaitTask 104 | let! docText = doc.GetTextAsync(ct) |> Async.AwaitTask 105 | 106 | let position = Position.toRoslynPosition docText.Lines p.Position 107 | let! symbolMaybe = SymbolFinder.FindSymbolAtPositionAsync(doc, position, ct) |> Async.AwaitTask 108 | let symbolIsFromMetadata = 109 | symbolMaybe 110 | |> Option.ofObj 111 | |> Option.map (fun s -> s.MetadataToken <> 0) 112 | |> Option.defaultValue false 113 | 114 | let linePositionSpan = 115 | Range.toLinePositionSpan docText.Lines { Start = p.Position; End = p.Position } 116 | 117 | let textSpan = docText.Lines.GetTextSpan(linePositionSpan) 118 | 119 | let! rootNode = docSyntaxTree.GetRootAsync(ct) |> Async.AwaitTask 120 | let nodeOnPos = 121 | rootNode.FindNode(textSpan, findInsideTrivia = false, getInnermostNodeForTie = true) 122 | 123 | let spanMaybe = 124 | match nodeOnPos with 125 | | :? PropertyDeclarationSyntax as propDec -> propDec.Identifier.Span |> Some 126 | | :? MethodDeclarationSyntax as methodDec -> methodDec.Identifier.Span |> Some 127 | | :? BaseTypeDeclarationSyntax as typeDec -> typeDec.Identifier.Span |> Some 128 | | :? VariableDeclaratorSyntax as varDec -> varDec.Identifier.Span |> Some 129 | | :? EnumMemberDeclarationSyntax as enumMemDec -> enumMemDec.Identifier.Span |> Some 130 | | :? ParameterSyntax as paramSyn -> paramSyn.Identifier.Span |> Some 131 | | :? NameSyntax as nameSyn -> nameSyn.Span |> Some 132 | | :? SingleVariableDesignationSyntax as designationSyn -> designationSyn.Identifier.Span |> Some 133 | | :? ForEachStatementSyntax as forEachSyn -> forEachSyn.Identifier.Span |> Some 134 | | :? LocalFunctionStatementSyntax as localFunStSyn -> localFunStSyn.Identifier.Span |> Some 135 | | node -> 136 | logger.debug ( 137 | Log.setMessage "textDocument/prepareRename: unhandled Type={type}" 138 | >> Log.addContext "type" (node.GetType().Name) 139 | ) 140 | None 141 | 142 | let rangeWithPlaceholderMaybe: PrepareRenameResult option = 143 | match spanMaybe, symbolIsFromMetadata with 144 | | Some span, false -> 145 | let range = Range.fromTextSpan docText.Lines span 146 | 147 | let text = docText.ToString(span) 148 | 149 | { Range = range; Placeholder = text } |> U3.C2 |> Some 150 | | _, _ -> None 151 | 152 | return rangeWithPlaceholderMaybe |> LspResult.success 153 | } 154 | 155 | let handle 156 | (context: ServerRequestContext) 157 | (p: RenameParams) 158 | : AsyncLspResult = async { 159 | match! context.FindSymbol' p.TextDocument.Uri p.Position with 160 | | None -> 161 | return None |> LspResult.success 162 | | Some (symbol, doc) -> 163 | let! ct = Async.CancellationToken 164 | let originalSolution = doc.Project.Solution 165 | 166 | let! updatedSolution = 167 | Renamer.RenameSymbolAsync( 168 | doc.Project.Solution, 169 | symbol, 170 | SymbolRenameOptions(RenameOverloads = true, RenameFile = true), 171 | p.NewName, 172 | ct 173 | ) 174 | |> Async.AwaitTask 175 | 176 | let! docTextEdit = 177 | lspDocChangesFromSolutionDiff 178 | ct 179 | originalSolution 180 | updatedSolution 181 | (fun uri -> context.OpenDocs.TryFind uri |> Option.map _.Version) 182 | 183 | return WorkspaceEdit.Create(docTextEdit, context.ClientCapabilities) |> Some |> LspResult.success 184 | } 185 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/SelectionRange.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | open CSharpLanguageServer.State 7 | 8 | [] 9 | module SelectionRange = 10 | let provider (_cc: ClientCapabilities) : bool option = None 11 | 12 | let registration (_cc: ClientCapabilities) : Registration option = None 13 | 14 | let handle (_ctx: ServerRequestContext) (_p: SelectionRangeParams) : AsyncLspResult = 15 | LspResult.notImplemented |> async.Return 16 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/SemanticTokens.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | open System.Collections.Generic 5 | 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | open Microsoft.CodeAnalysis.Classification 10 | open Microsoft.CodeAnalysis.Text 11 | 12 | open CSharpLanguageServer.State 13 | open CSharpLanguageServer.Util 14 | open CSharpLanguageServer.Conversions 15 | open CSharpLanguageServer.Types 16 | 17 | [] 18 | module SemanticTokens = 19 | let private classificationTypeMap = Map [ 20 | (ClassificationTypeNames.ClassName, "class"); 21 | (ClassificationTypeNames.Comment, "comment"); 22 | (ClassificationTypeNames.ConstantName, "property"); 23 | (ClassificationTypeNames.ControlKeyword, "keyword"); 24 | (ClassificationTypeNames.DelegateName, "class"); 25 | (ClassificationTypeNames.EnumMemberName, "enumMember"); 26 | (ClassificationTypeNames.EnumName, "enum"); 27 | (ClassificationTypeNames.EventName, "event"); 28 | (ClassificationTypeNames.ExtensionMethodName, "method"); 29 | (ClassificationTypeNames.FieldName, "property"); 30 | (ClassificationTypeNames.Identifier, "variable"); 31 | (ClassificationTypeNames.InterfaceName, "interface"); 32 | (ClassificationTypeNames.LabelName, "variable"); 33 | (ClassificationTypeNames.LocalName, "variable"); 34 | (ClassificationTypeNames.Keyword, "keyword"); 35 | (ClassificationTypeNames.MethodName, "method"); 36 | (ClassificationTypeNames.NamespaceName, "namespace"); 37 | (ClassificationTypeNames.NumericLiteral, "number"); 38 | (ClassificationTypeNames.Operator, "operator"); 39 | (ClassificationTypeNames.OperatorOverloaded, "operator"); 40 | (ClassificationTypeNames.ParameterName, "parameter"); 41 | (ClassificationTypeNames.PropertyName, "property"); 42 | (ClassificationTypeNames.RecordClassName, "class"); 43 | (ClassificationTypeNames.RecordStructName, "struct"); 44 | (ClassificationTypeNames.RegexText, "regex"); 45 | (ClassificationTypeNames.StringLiteral, "string"); 46 | (ClassificationTypeNames.StructName, "struct"); 47 | (ClassificationTypeNames.TypeParameterName, "typeParameter"); 48 | (ClassificationTypeNames.VerbatimStringLiteral, "string") 49 | ] 50 | 51 | let private classificationModifierMap = Map [ 52 | (ClassificationTypeNames.StaticSymbol, "static") 53 | ] 54 | 55 | let private semanticTokenTypeMap = 56 | classificationTypeMap 57 | |> Map.values 58 | |> Seq.distinct 59 | |> flip Seq.zip (Seq.initInfinite uint32) 60 | |> Map.ofSeq 61 | 62 | let private semanticTokenModifierMap = 63 | classificationModifierMap 64 | |> Map.values 65 | |> Seq.distinct 66 | |> flip Seq.zip (Seq.initInfinite uint32) 67 | |> Map.ofSeq 68 | 69 | let private semanticTokenTypes = 70 | semanticTokenTypeMap 71 | |> Seq.sortBy (fun kvp -> kvp.Value) 72 | |> Seq.map (fun kvp -> kvp.Key) 73 | 74 | let private semanticTokenModifiers = 75 | semanticTokenModifierMap 76 | |> Seq.sortBy (fun kvp -> kvp.Value) 77 | |> Seq.map (fun kvp -> kvp.Key) 78 | 79 | let private getSemanticTokenIdFromClassification (classification: string) = 80 | classificationTypeMap 81 | |> Map.tryFind classification 82 | |> Option.bind (flip Map.tryFind semanticTokenTypeMap) 83 | 84 | let private getSemanticTokenModifierFlagFromClassification (classification: string) = 85 | classificationModifierMap 86 | |> Map.tryFind classification 87 | |> Option.bind (flip Map.tryFind semanticTokenModifierMap) 88 | |> Option.defaultValue 0u 89 | |> int32 90 | |> (<<<) 1u 91 | 92 | let private toSemanticToken (lines: TextLineCollection) (textSpan: TextSpan, spans: IEnumerable) = 93 | let (typeId, modifiers) = 94 | spans 95 | |> Seq.fold (fun (t, m) s -> 96 | if ClassificationTypeNames.AdditiveTypeNames.Contains(s.ClassificationType) then 97 | (t, m ||| (getSemanticTokenModifierFlagFromClassification s.ClassificationType)) 98 | else 99 | (getSemanticTokenIdFromClassification s.ClassificationType, m) 100 | ) (None, 0u) 101 | let pos = lines.GetLinePositionSpan(textSpan) 102 | (uint32 pos.Start.Line, uint32 pos.Start.Character, uint32 (pos.End.Character - pos.Start.Character), typeId, modifiers) 103 | 104 | let private computePosition (((pLine, pChar, _, _, _), (cLine, cChar, cLen, cToken, cModifiers)): ((uint32 * uint32 * uint32 * uint32 * uint32) * (uint32 * uint32 * uint32 * uint32 * uint32))) = 105 | let deltaLine = cLine - pLine 106 | let deltaChar = 107 | if deltaLine = 0u then 108 | cChar - pChar 109 | else 110 | cChar 111 | (deltaLine, deltaChar, cLen, cToken, cModifiers) 112 | 113 | let private getSemanticTokensRange (context: ServerRequestContext) (uri: string) (range: Range option): AsyncLspResult = async { 114 | let docMaybe = context.GetUserDocument uri 115 | match docMaybe with 116 | | None -> 117 | return None |> LspResult.success 118 | | Some doc -> 119 | let! ct = Async.CancellationToken 120 | let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask 121 | let textSpan = 122 | range 123 | |> Option.map (Range.toTextSpan sourceText.Lines) 124 | |> Option.defaultValue (TextSpan(0, sourceText.Length)) 125 | let! spans = Classifier.GetClassifiedSpansAsync(doc, textSpan, ct) |> Async.AwaitTask 126 | let tokens = 127 | spans 128 | |> Seq.groupBy (fun span -> span.TextSpan) 129 | |> Seq.map (toSemanticToken sourceText.Lines) 130 | |> Seq.filter (fun (_, _, _, oi, _) -> Option.isSome oi) 131 | |> Seq.map (fun (line, startChar, len, tokenId, modifiers) -> (line, startChar, len, Option.get tokenId, modifiers)) 132 | 133 | let response = 134 | { Data = 135 | Seq.zip (seq {yield (0u,0u,0u,0u,0u); yield! tokens}) tokens 136 | |> Seq.map computePosition 137 | |> Seq.map (fun (a,b,c,d,e) -> [a;b;c;d;e]) 138 | |> Seq.concat 139 | |> Seq.toArray 140 | ResultId = None } // TODO: add a result id after we support delta semantic tokens 141 | return Some response |> LspResult.success 142 | } 143 | 144 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 145 | clientCapabilities.TextDocument 146 | |> Option.bind (fun x -> x.SemanticTokens) 147 | |> Option.bind (fun x -> x.DynamicRegistration) 148 | |> Option.defaultValue false 149 | 150 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 151 | match dynamicRegistration clientCapabilities with 152 | | true -> None 153 | | false -> 154 | let semanticTokensOptions: SemanticTokensOptions = 155 | { Legend = { TokenTypes = semanticTokenTypes |> Seq.toArray 156 | TokenModifiers = semanticTokenModifiers |> Seq.toArray } 157 | Range = Some (U2.C1 true) 158 | Full = Some (U2.C1 true) 159 | WorkDoneProgress = None 160 | } 161 | 162 | Some (U2.C1 semanticTokensOptions) 163 | 164 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 165 | match dynamicRegistration clientCapabilities with 166 | | false -> None 167 | | true -> 168 | let registerOptions: SemanticTokensRegistrationOptions = 169 | { Legend = { TokenTypes = semanticTokenTypes |> Seq.toArray 170 | TokenModifiers = semanticTokenModifiers |> Seq.toArray } 171 | Range = Some (U2.C1 true) 172 | Full = Some (U2.C1 true) 173 | Id = None 174 | WorkDoneProgress = None 175 | DocumentSelector = Some defaultDocumentSelector } 176 | Some 177 | { Id = Guid.NewGuid().ToString() 178 | Method = "textDocument/semanticTokens" 179 | RegisterOptions = registerOptions |> serialize |> Some } 180 | 181 | // TODO: Everytime the server will re-compute semantic tokens, is it possible to cache the result? 182 | let handleFull (context: ServerRequestContext) (p: SemanticTokensParams): AsyncLspResult = 183 | getSemanticTokensRange context p.TextDocument.Uri None 184 | 185 | let handleFullDelta 186 | (_context: ServerRequestContext) 187 | (_p: SemanticTokensDeltaParams) 188 | : AsyncLspResult option> = 189 | LspResult.notImplemented option> 190 | |> async.Return 191 | 192 | let handleRange (context: ServerRequestContext) (p: SemanticTokensRangeParams): AsyncLspResult = 193 | getSemanticTokensRange context p.TextDocument.Uri (Some p.Range) 194 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/SignatureHelp.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis 6 | open Microsoft.CodeAnalysis.CSharp 7 | open Microsoft.CodeAnalysis.CSharp.Syntax 8 | open Ionide.LanguageServerProtocol.Server 9 | open Ionide.LanguageServerProtocol.Types 10 | open Ionide.LanguageServerProtocol.JsonRpc 11 | 12 | open CSharpLanguageServer 13 | open CSharpLanguageServer.State 14 | open CSharpLanguageServer.Util 15 | open CSharpLanguageServer.Conversions 16 | open CSharpLanguageServer.Types 17 | 18 | module SignatureInformation = 19 | let internal fromMethod (m: IMethodSymbol) = 20 | let parameters = 21 | m.Parameters 22 | |> Seq.map (fun p -> 23 | { Label = SymbolName.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat p |> U2.C1 24 | Documentation = None }) 25 | |> Array.ofSeq 26 | 27 | let documentation = 28 | { Kind = MarkupKind.Markdown 29 | Value = DocumentationUtil.markdownDocForSymbol m } 30 | |> U2.C2 31 | 32 | { Label = SymbolName.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat m 33 | Documentation = Some documentation 34 | Parameters = Some parameters 35 | ActiveParameter = None } 36 | 37 | [] 38 | module SignatureHelp = 39 | type InvocationContext = 40 | { Receiver: SyntaxNode 41 | ArgumentTypes: TypeInfo list 42 | Separators: SyntaxToken list } 43 | 44 | // Algorithm from omnisharp-roslyn (https://github.com/OmniSharp/omnisharp-roslyn/blob/2d582b05839dbd23baf6e78fa2279163723a824c/src/OmniSharp.Roslyn.CSharp/Services/Signatures/SignatureHelpService.cs#L139C1-L166C10) 45 | let private methodScore (types: TypeInfo list) (m: IMethodSymbol) = 46 | let score (invocation: TypeInfo) (definition: IParameterSymbol) = 47 | if isNull invocation.ConvertedType then 48 | 1 49 | else if SymbolEqualityComparer.Default.Equals(invocation.ConvertedType, definition.Type) then 50 | 2 51 | else 52 | 0 53 | 54 | if m.Parameters.Length < types.Length then 55 | Microsoft.FSharp.Core.int.MinValue 56 | else 57 | Seq.zip types m.Parameters |> Seq.map (uncurry score) |> Seq.sum 58 | 59 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 60 | clientCapabilities.TextDocument 61 | |> Option.bind (fun x -> x.SignatureHelp) 62 | |> Option.bind (fun x -> x.DynamicRegistration) 63 | |> Option.defaultValue false 64 | 65 | let provider (clientCapabilities: ClientCapabilities) : SignatureHelpOptions option = 66 | match dynamicRegistration clientCapabilities with 67 | | true -> None 68 | | false -> 69 | Some 70 | { TriggerCharacters = Some([| "("; ","; "<"; "{"; "[" |]) 71 | WorkDoneProgress = None 72 | RetriggerCharacters = None } 73 | 74 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 75 | match dynamicRegistration clientCapabilities with 76 | | false -> None 77 | | true -> 78 | let registerOptions: SignatureHelpRegistrationOptions = 79 | { TriggerCharacters = Some([| "("; ","; "<"; "{"; "[" |]) 80 | RetriggerCharacters = None 81 | WorkDoneProgress = None 82 | DocumentSelector = Some defaultDocumentSelector } 83 | Some 84 | { Id = Guid.NewGuid().ToString() 85 | Method = "textDocument/signatureHelp" 86 | RegisterOptions = registerOptions |> serialize |> Some } 87 | 88 | let handle (context: ServerRequestContext) (p: SignatureHelpParams): AsyncLspResult = async { 89 | let docMaybe = context.GetUserDocument p.TextDocument.Uri 90 | match docMaybe with 91 | | None -> return None |> LspResult.success 92 | | Some doc -> 93 | let! ct = Async.CancellationToken 94 | let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask 95 | let! semanticModel = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask 96 | 97 | let position = Position.toRoslynPosition sourceText.Lines p.Position 98 | 99 | let! syntaxTree = doc.GetSyntaxTreeAsync(ct) |> Async.AwaitTask 100 | let! root = syntaxTree.GetRootAsync(ct) |> Async.AwaitTask 101 | 102 | let rec findInvocationContext (node: SyntaxNode): InvocationContext option = 103 | match node with 104 | | :? InvocationExpressionSyntax as invocation when invocation.ArgumentList.Span.Contains(position) -> 105 | Some { Receiver = invocation.Expression 106 | ArgumentTypes = invocation.ArgumentList.Arguments |> Seq.map (fun a -> semanticModel.GetTypeInfo(a.Expression)) |> List.ofSeq 107 | Separators = invocation.ArgumentList.Arguments.GetSeparators() |> List.ofSeq } 108 | 109 | | :? BaseObjectCreationExpressionSyntax as objectCreation when 110 | objectCreation.ArgumentList 111 | |> Option.ofObj 112 | |> Option.map (fun argList -> argList.Span.Contains(position)) 113 | |> Option.defaultValue false 114 | -> 115 | Some { Receiver = objectCreation 116 | ArgumentTypes = objectCreation.ArgumentList.Arguments |> Seq.map (fun a -> semanticModel.GetTypeInfo(a.Expression)) |> List.ofSeq 117 | Separators = objectCreation.ArgumentList.Arguments.GetSeparators() |> List.ofSeq } 118 | 119 | | :? AttributeSyntax as attributeSyntax when 120 | attributeSyntax.ArgumentList 121 | |> Option.ofObj 122 | |> Option.map (fun argList -> argList.Span.Contains(position)) 123 | |> Option.defaultValue false 124 | -> 125 | Some { Receiver = attributeSyntax 126 | ArgumentTypes = attributeSyntax.ArgumentList.Arguments |> Seq.map (fun a -> semanticModel.GetTypeInfo(a.Expression)) |> List.ofSeq 127 | Separators = attributeSyntax.ArgumentList.Arguments.GetSeparators() |> List.ofSeq } 128 | 129 | | _ -> 130 | node 131 | |> Option.ofObj 132 | |> Option.bind (fun node -> findInvocationContext node.Parent) 133 | 134 | match root.FindToken(position).Parent |> findInvocationContext with 135 | | None -> return None |> LspResult.success 136 | | Some invocation -> 137 | let methodGroup = 138 | semanticModel.GetMemberGroup(invocation.Receiver).OfType() 139 | |> List.ofSeq 140 | 141 | let matchingMethodMaybe = 142 | methodGroup 143 | |> Seq.map (fun m -> m, methodScore (invocation.ArgumentTypes) m) 144 | |> Seq.tryMaxBy snd 145 | |> Option.map fst 146 | 147 | let activeParameterMaybe = 148 | invocation.Separators 149 | |> List.tryFindIndex (fun comma -> comma.Span.Start >= position) 150 | |> Option.map uint 151 | 152 | let signatureHelpResult = 153 | { Signatures = methodGroup |> Seq.map SignatureInformation.fromMethod |> Array.ofSeq 154 | ActiveSignature = matchingMethodMaybe |> Option.map (fun m -> List.findIndex ((=) m) methodGroup |> uint32) 155 | ActiveParameter = activeParameterMaybe } 156 | 157 | return Some signatureHelpResult |> LspResult.success 158 | } 159 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/TextDocumentSync.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis.Text 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | 10 | open CSharpLanguageServer 11 | open CSharpLanguageServer.Conversions 12 | open CSharpLanguageServer.State 13 | open CSharpLanguageServer.State.ServerState 14 | open CSharpLanguageServer.Types 15 | open CSharpLanguageServer.RoslynHelpers 16 | open CSharpLanguageServer.Logging 17 | 18 | [] 19 | module TextDocumentSync = 20 | let private logger = LogProvider.getLoggerByName "TextDocumentSync" 21 | 22 | let private applyLspContentChangesOnRoslynSourceText 23 | (changes: TextDocumentContentChangeEvent[]) 24 | (initialSourceText: SourceText) = 25 | 26 | let applyLspContentChangeOnRoslynSourceText (sourceText: SourceText) (change: TextDocumentContentChangeEvent) = 27 | match change with 28 | | U2.C1 change -> 29 | let changeTextSpan = 30 | change.Range 31 | |> Range.toLinePositionSpan sourceText.Lines 32 | |> sourceText.Lines.GetTextSpan 33 | 34 | TextChange(changeTextSpan, change.Text) |> sourceText.WithChanges 35 | | U2.C2 changeWoRange -> 36 | SourceText.From(changeWoRange.Text) 37 | 38 | changes |> Seq.fold applyLspContentChangeOnRoslynSourceText initialSourceText 39 | 40 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 41 | clientCapabilities.TextDocument 42 | |> Option.bind (fun x -> x.Synchronization) 43 | |> Option.bind (fun x -> x.DynamicRegistration) 44 | |> Option.defaultValue false 45 | 46 | let provider (clientCapabilities: ClientCapabilities) : TextDocumentSyncOptions option = 47 | match dynamicRegistration clientCapabilities with 48 | | true -> None 49 | | false -> 50 | Some 51 | { TextDocumentSyncOptions.Default with 52 | OpenClose = Some true 53 | Save = Some (U2.C2 { IncludeText = Some true }) 54 | Change = Some TextDocumentSyncKind.Incremental } 55 | 56 | let didOpenRegistration (clientCapabilities: ClientCapabilities) : Registration option = 57 | match dynamicRegistration clientCapabilities with 58 | | false -> None 59 | | true -> 60 | let registerOptions = { 61 | DocumentSelector = Some defaultDocumentSelector } 62 | 63 | Some 64 | { Id = Guid.NewGuid().ToString() 65 | Method = "textDocument/didOpen" 66 | RegisterOptions = registerOptions |> serialize |> Some } 67 | 68 | 69 | let didChangeRegistration (clientCapabilities: ClientCapabilities) : Registration option = 70 | match dynamicRegistration clientCapabilities with 71 | | false -> None 72 | | true -> 73 | let registerOptions = 74 | { DocumentSelector = Some defaultDocumentSelector 75 | SyncKind = TextDocumentSyncKind.Incremental } 76 | Some 77 | { Id = Guid.NewGuid().ToString() 78 | Method = "textDocument/didChange" 79 | RegisterOptions = registerOptions|> serialize |> Some } 80 | 81 | let didSaveRegistration (clientCapabilities: ClientCapabilities) : Registration option = 82 | match dynamicRegistration clientCapabilities with 83 | | false -> None 84 | | true -> 85 | let registerOptions = 86 | { DocumentSelector = Some defaultDocumentSelector 87 | IncludeText = Some true } 88 | 89 | Some 90 | { Id = Guid.NewGuid().ToString() 91 | Method = "textDocument/didSave" 92 | RegisterOptions = registerOptions |> serialize |> Some } 93 | 94 | let didCloseRegistration (clientCapabilities: ClientCapabilities) : Registration option = 95 | match dynamicRegistration clientCapabilities with 96 | | false -> None 97 | | true -> 98 | let registerOptions = { 99 | DocumentSelector = Some defaultDocumentSelector } 100 | 101 | Some 102 | { Id = Guid.NewGuid().ToString() 103 | Method = "textDocument/didClose" 104 | RegisterOptions = registerOptions |> serialize |> Some } 105 | 106 | let willSaveRegistration (_clientCapabilities: ClientCapabilities) : Registration option = None 107 | 108 | let willSaveWaitUntilRegistration (_clientCapabilities: ClientCapabilities) : Registration option = None 109 | 110 | let didOpen (context: ServerRequestContext) 111 | (openParams: DidOpenTextDocumentParams) 112 | : Async> = 113 | match context.GetDocumentForUriOfType AnyDocument openParams.TextDocument.Uri with 114 | | Some (doc, docType) -> 115 | match docType with 116 | | UserDocument -> 117 | // we want to load the document in case it has been changed since we have the solution loaded 118 | // also, as a bonus we can recover from corrupted document view in case document in roslyn solution 119 | // went out of sync with editor 120 | let updatedDoc = SourceText.From(openParams.TextDocument.Text) |> doc.WithText 121 | 122 | context.Emit(OpenDocAdd (openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) 123 | context.Emit(SolutionChange updatedDoc.Project.Solution) 124 | 125 | Ok() |> async.Return 126 | 127 | | _ -> 128 | Ok() |> async.Return 129 | 130 | | None -> 131 | let docFilePathMaybe = Util.tryParseFileUri openParams.TextDocument.Uri 132 | 133 | match docFilePathMaybe with 134 | | Some docFilePath -> async { 135 | // ok, this document is not in solution, register a new document 136 | let! newDocMaybe = 137 | tryAddDocument 138 | logger 139 | docFilePath 140 | openParams.TextDocument.Text 141 | context.Solution 142 | 143 | match newDocMaybe with 144 | | Some newDoc -> 145 | context.Emit(OpenDocAdd (openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) 146 | context.Emit(SolutionChange newDoc.Project.Solution) 147 | 148 | | None -> () 149 | 150 | return Ok() 151 | } 152 | 153 | | None -> 154 | Ok() |> async.Return 155 | 156 | let didChange (context: ServerRequestContext) 157 | (changeParams: DidChangeTextDocumentParams) 158 | : Async> = 159 | async { 160 | let docMaybe = context.GetUserDocument changeParams.TextDocument.Uri 161 | match docMaybe with 162 | | None -> () 163 | | Some doc -> 164 | let! ct = Async.CancellationToken 165 | let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask 166 | //logMessage (sprintf "TextDocumentDidChange: changeParams: %s" (string changeParams)) 167 | //logMessage (sprintf "TextDocumentDidChange: sourceText: %s" (string sourceText)) 168 | 169 | let updatedSourceText = sourceText |> applyLspContentChangesOnRoslynSourceText changeParams.ContentChanges 170 | let updatedDoc = doc.WithText(updatedSourceText) 171 | 172 | //logMessage (sprintf "TextDocumentDidChange: newSourceText: %s" (string updatedSourceText)) 173 | 174 | let updatedSolution = updatedDoc.Project.Solution 175 | 176 | context.Emit(SolutionChange updatedSolution) 177 | context.Emit(OpenDocAdd (changeParams.TextDocument.Uri, changeParams.TextDocument.Version, DateTime.Now)) 178 | 179 | return Ok() 180 | } 181 | 182 | let didClose (context: ServerRequestContext) 183 | (closeParams: DidCloseTextDocumentParams) 184 | : Async> = 185 | context.Emit(OpenDocRemove closeParams.TextDocument.Uri) 186 | Ok() |> async.Return 187 | 188 | let willSave (_context: ServerRequestContext) (_p: WillSaveTextDocumentParams): Async> = async { 189 | return Ok () 190 | } 191 | 192 | let willSaveWaitUntil (_context: ServerRequestContext) (_p: WillSaveTextDocumentParams): AsyncLspResult = async { 193 | return LspResult.notImplemented 194 | } 195 | 196 | let didSave (context: ServerRequestContext) 197 | (saveParams: DidSaveTextDocumentParams) 198 | : Async> = 199 | // we need to add this file to solution if not already 200 | let doc = context.GetDocument saveParams.TextDocument.Uri 201 | 202 | match doc with 203 | | Some _ -> 204 | Ok() |> async.Return 205 | 206 | | None -> async { 207 | let docFilePath = Util.parseFileUri saveParams.TextDocument.Uri 208 | let! newDocMaybe = 209 | tryAddDocument 210 | logger 211 | docFilePath 212 | saveParams.Text.Value 213 | context.Solution 214 | 215 | match newDocMaybe with 216 | | Some newDoc -> 217 | context.Emit(OpenDocTouch (saveParams.TextDocument.Uri, DateTime.Now)) 218 | context.Emit(SolutionChange newDoc.Project.Solution) 219 | 220 | | None -> () 221 | 222 | return Ok() 223 | } 224 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/TypeDefinition.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | 10 | open CSharpLanguageServer.Types 11 | open CSharpLanguageServer.State 12 | open CSharpLanguageServer.Util 13 | 14 | [] 15 | module TypeDefinition = 16 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) : bool = 17 | clientCapabilities.TextDocument 18 | |> Option.bind (fun x -> x.TypeDefinition) 19 | |> Option.bind (fun x -> x.DynamicRegistration) 20 | |> Option.defaultValue false 21 | 22 | let provider (clientCapabilities: ClientCapabilities) : U3 option = 23 | match dynamicRegistration clientCapabilities with 24 | | true -> None 25 | | false -> Some (U3.C1 true) 26 | 27 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 28 | match dynamicRegistration clientCapabilities with 29 | | false -> None 30 | | true -> 31 | let registerOptions: TypeDefinitionRegistrationOptions = 32 | { DocumentSelector = Some defaultDocumentSelector 33 | Id = None 34 | WorkDoneProgress = None } 35 | Some { 36 | Id = Guid.NewGuid().ToString() 37 | Method = "textDocument/typeDefinition" 38 | RegisterOptions = registerOptions |> serialize |> Some } 39 | 40 | let handle (context: ServerRequestContext) (p: TypeDefinitionParams) : Async option>> = async { 41 | match! context.FindSymbol' p.TextDocument.Uri p.Position with 42 | | None -> 43 | return None |> LspResult.success 44 | | Some (symbol, doc) -> 45 | let typeSymbol = 46 | match symbol with 47 | | :? ILocalSymbol as localSymbol -> [localSymbol.Type] 48 | | :? IFieldSymbol as fieldSymbol -> [fieldSymbol.Type] 49 | | :? IPropertySymbol as propertySymbol -> [propertySymbol.Type] 50 | | :? IParameterSymbol as parameterSymbol -> [parameterSymbol.Type] 51 | | _ -> [] 52 | let! locations = 53 | typeSymbol 54 | |> Seq.map (flip context.ResolveSymbolLocations (Some doc.Project)) 55 | |> Async.Parallel 56 | |> Async.map (Seq.collect id >> Seq.toArray) 57 | return 58 | locations 59 | |> Declaration.C2 60 | |> U2.C1 61 | |> Some 62 | |> LspResult.success 63 | } 64 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/TypeHierarchy.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | 10 | open CSharpLanguageServer.Types 11 | open CSharpLanguageServer.State 12 | open CSharpLanguageServer.Conversions 13 | open CSharpLanguageServer.Util 14 | 15 | [] 16 | module TypeHierarchy = 17 | let private isTypeSymbol (symbol: ISymbol) = 18 | match symbol with 19 | | :? INamedTypeSymbol -> true 20 | | _ -> false 21 | 22 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 23 | clientCapabilities.TextDocument 24 | |> Option.bind (fun x -> x.TypeHierarchy) 25 | |> Option.bind (fun x -> x.DynamicRegistration) 26 | |> Option.defaultValue false 27 | 28 | let provider (clientCapabilities: ClientCapabilities) : U3 option = 29 | match dynamicRegistration clientCapabilities with 30 | | true -> None 31 | | false -> Some (U3.C1 true) 32 | 33 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 34 | match dynamicRegistration clientCapabilities with 35 | | false -> None 36 | | true -> 37 | let registerOptions: TypeHierarchyRegistrationOptions = 38 | { DocumentSelector = Some defaultDocumentSelector 39 | Id = None 40 | WorkDoneProgress = None } 41 | Some 42 | { Id = Guid.NewGuid().ToString() 43 | Method = "textDocument/prepareTypeHierarchy" 44 | RegisterOptions = registerOptions |> serialize |> Some } 45 | 46 | let prepare (context: ServerRequestContext) (p: TypeHierarchyPrepareParams) : AsyncLspResult = async { 47 | match! context.FindSymbol p.TextDocument.Uri p.Position with 48 | | Some symbol when isTypeSymbol symbol -> 49 | let! itemList = TypeHierarchyItem.fromSymbol context.ResolveSymbolLocations symbol 50 | return itemList |> List.toArray |> Some |> LspResult.success 51 | | _ -> 52 | return None |> LspResult.success 53 | } 54 | 55 | let supertypes 56 | (context: ServerRequestContext) 57 | (p: TypeHierarchySupertypesParams) 58 | : AsyncLspResult = async { 59 | match! context.FindSymbol p.Item.Uri p.Item.Range.Start with 60 | | Some symbol when isTypeSymbol symbol -> 61 | let typeSymbol = symbol :?> INamedTypeSymbol 62 | let baseType = 63 | typeSymbol.BaseType 64 | |> Option.ofObj 65 | |> Option.filter (fun sym -> sym.SpecialType = SpecialType.None) 66 | |> Option.toList 67 | let interfaces = Seq.toList typeSymbol.Interfaces 68 | let supertypes = baseType @ interfaces 69 | let! items = supertypes |> Seq.map (TypeHierarchyItem.fromSymbol context.ResolveSymbolLocations) |> Async.Parallel 70 | return items |> Seq.collect id |> Seq.toArray |> Some |> LspResult.success 71 | | _ -> return None |> LspResult.success 72 | } 73 | 74 | let subtypes (context: ServerRequestContext) (p: TypeHierarchySubtypesParams) : AsyncLspResult = async { 75 | match! context.FindSymbol p.Item.Uri p.Item.Range.Start with 76 | | Some symbol when isTypeSymbol symbol -> 77 | let typeSymbol = symbol :?> INamedTypeSymbol 78 | // We only want immediately derived classes/interfaces/implementations here (we only need 79 | // subclasses not subclasses' subclasses) 80 | let! subtypes = 81 | [ context.FindDerivedClasses' typeSymbol false 82 | context.FindDerivedInterfaces' typeSymbol false 83 | context.FindImplementations' typeSymbol false ] 84 | |> Async.Parallel 85 | |> Async.map (Seq.collect id >> Seq.toList) 86 | let! items = subtypes |> Seq.map (TypeHierarchyItem.fromSymbol context.ResolveSymbolLocations) |> Async.Parallel 87 | return items |> Seq.collect id |> Seq.toArray |> Some |> LspResult.success 88 | | _ -> 89 | return None |> LspResult.success 90 | } 91 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/Workspace.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | open System.IO 5 | 6 | open Ionide.LanguageServerProtocol.Types 7 | open Ionide.LanguageServerProtocol.JsonRpc 8 | open Ionide.LanguageServerProtocol.Server 9 | open Microsoft.CodeAnalysis.Text 10 | 11 | open CSharpLanguageServer 12 | open CSharpLanguageServer.State 13 | open CSharpLanguageServer.State.ServerState 14 | open CSharpLanguageServer.RoslynHelpers 15 | open CSharpLanguageServer.Logging 16 | open CSharpLanguageServer.Types 17 | 18 | [] 19 | module Workspace = 20 | let private logger = LogProvider.getLoggerByName "Workspace" 21 | 22 | let dynamicRegistration (clientCapabilities: ClientCapabilities) = 23 | clientCapabilities.Workspace 24 | |> Option.bind (fun x -> x.DidChangeWatchedFiles) 25 | |> Option.bind (fun x -> x.DynamicRegistration) 26 | |> Option.defaultValue false 27 | 28 | let registration (clientCapabilities: ClientCapabilities): Registration option = 29 | match dynamicRegistration clientCapabilities with 30 | | false -> None 31 | | true -> 32 | let fileSystemWatcher = 33 | { GlobPattern = U2.C1 "**/*.{cs,csproj,sln,slnx}" 34 | Kind = Some (WatchKind.Create ||| WatchKind.Change ||| WatchKind.Delete) } 35 | 36 | let registerOptions: DidChangeWatchedFilesRegistrationOptions = 37 | { Watchers = [| fileSystemWatcher |] } 38 | 39 | Some 40 | { Id = Guid.NewGuid().ToString() 41 | Method = "workspace/didChangeWatchedFiles" 42 | RegisterOptions = registerOptions |> serialize |> Some } 43 | 44 | let private tryReloadDocumentOnUri logger (context: ServerRequestContext) uri = async { 45 | match context.GetUserDocument uri with 46 | | Some doc -> 47 | let fileText = uri |> Util.parseFileUri |> File.ReadAllText 48 | let updatedDoc = SourceText.From(fileText) |> doc.WithText 49 | 50 | context.Emit(SolutionChange updatedDoc.Project.Solution) 51 | 52 | | None -> 53 | let docFilePathMaybe = uri |> Util.tryParseFileUri 54 | match docFilePathMaybe with 55 | | Some docFilePath -> 56 | // ok, this document is not on solution, register a new one 57 | let fileText = docFilePath |> File.ReadAllText 58 | let! newDocMaybe = tryAddDocument logger 59 | docFilePath 60 | fileText 61 | context.Solution 62 | match newDocMaybe with 63 | | Some newDoc -> 64 | context.Emit(SolutionChange newDoc.Project.Solution) 65 | | None -> () 66 | | None -> () 67 | } 68 | 69 | let private removeDocument (context: ServerRequestContext) uri = 70 | match context.GetUserDocument uri with 71 | | Some existingDoc -> 72 | let updatedProject = existingDoc.Project.RemoveDocument(existingDoc.Id) 73 | 74 | context.Emit(SolutionChange updatedProject.Solution) 75 | context.Emit(OpenDocRemove uri) 76 | | None -> () 77 | 78 | let didChangeWatchedFiles (context: ServerRequestContext) 79 | (p: DidChangeWatchedFilesParams) 80 | : Async> = async { 81 | for change in p.Changes do 82 | match Path.GetExtension(change.Uri) with 83 | | ".csproj" -> 84 | do! context.WindowShowMessage "change to .csproj detected, will reload solution" 85 | context.Emit(SolutionReloadRequest (TimeSpan.FromSeconds(5:int64))) 86 | 87 | | ".sln" | ".slnx" -> 88 | do! context.WindowShowMessage "change to .sln detected, will reload solution" 89 | context.Emit(SolutionReloadRequest (TimeSpan.FromSeconds(5:int64))) 90 | 91 | | ".cs" -> 92 | match change.Type with 93 | | FileChangeType.Created -> 94 | do! tryReloadDocumentOnUri logger context change.Uri 95 | | FileChangeType.Changed -> 96 | do! tryReloadDocumentOnUri logger context change.Uri 97 | | FileChangeType.Deleted -> 98 | do removeDocument context change.Uri 99 | | _ -> () 100 | 101 | | _ -> () 102 | 103 | return Ok() 104 | } 105 | 106 | let didChangeConfiguration (context: ServerRequestContext) 107 | (configParams: DidChangeConfigurationParams) 108 | : Async> = async { 109 | 110 | let csharpSettings = 111 | configParams.Settings 112 | |> deserialize 113 | |> (fun x -> x.csharp) 114 | |> Option.defaultValue ServerSettingsCSharpDto.Default 115 | 116 | let newServerSettings = { 117 | context.State.Settings with 118 | SolutionPath = csharpSettings.solution 119 | ApplyFormattingOptions = csharpSettings.applyFormattingOptions 120 | |> Option.defaultValue false 121 | } 122 | 123 | context.Emit(SettingsChange newServerSettings) 124 | 125 | return Ok() 126 | } 127 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Handlers 2 | 3 | open System 4 | 5 | open Microsoft.CodeAnalysis 6 | open Ionide.LanguageServerProtocol.Server 7 | open Ionide.LanguageServerProtocol.Types 8 | open Ionide.LanguageServerProtocol.JsonRpc 9 | 10 | open CSharpLanguageServer.State 11 | open CSharpLanguageServer.Conversions 12 | 13 | [] 14 | module WorkspaceSymbol = 15 | let private dynamicRegistration (clientCapabilities: ClientCapabilities) = 16 | clientCapabilities.TextDocument 17 | |> Option.bind (fun x -> x.Formatting) 18 | |> Option.bind (fun x -> x.DynamicRegistration) 19 | |> Option.defaultValue false 20 | 21 | let provider (clientCapabilities: ClientCapabilities) : U2 option = 22 | match dynamicRegistration clientCapabilities with 23 | | true -> None 24 | | false -> U2.C1 true |> Some 25 | 26 | let registration (clientCapabilities: ClientCapabilities) : Registration option = 27 | match dynamicRegistration clientCapabilities with 28 | | false -> None 29 | | true -> 30 | let registerOptions: WorkspaceSymbolRegistrationOptions = 31 | { ResolveProvider = Some true 32 | WorkDoneProgress = None 33 | } 34 | 35 | Some 36 | { Id = Guid.NewGuid().ToString() 37 | Method = "workspace/symbol" 38 | RegisterOptions = registerOptions |> serialize |> Some } 39 | 40 | let handle (context: ServerRequestContext) (p: WorkspaceSymbolParams) : AsyncLspResult option> = async { 41 | let pattern = 42 | if String.IsNullOrEmpty(p.Query) then 43 | None 44 | else 45 | Some p.Query 46 | let! symbols = context.FindSymbols pattern 47 | return 48 | symbols 49 | |> Seq.map (SymbolInformation.fromSymbol SymbolDisplayFormat.MinimallyQualifiedFormat) 50 | |> Seq.collect id 51 | // TODO: make 100 configurable? 52 | |> Seq.truncate 100 53 | |> Seq.toArray 54 | |> U2.C1 55 | |> Some 56 | |> LspResult.success 57 | } 58 | 59 | let resolve (_context: ServerRequestContext) (_p: WorkspaceSymbol) : AsyncLspResult = 60 | LspResult.notImplemented |> async.Return 61 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/LruCache.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer 2 | 3 | // It's more OO than functional, but it makes user happier than a functional & immutable version. 4 | // In LSP, there will not be lot of cocurrent requests, so a simple cache based on LRU should be enough, and the size is 5 | // usually small. 6 | type LruCache<'entry>(size: int) = 7 | 8 | let lockObj : obj = new obj() 9 | 10 | let mutable key = 0UL 11 | 12 | let mutable list : (uint64 * 'entry) list = list.Empty 13 | 14 | member this.add(entry: 'entry) : uint64 = 15 | lock lockObj (fun () -> 16 | let k = key 17 | key <- key + 1UL 18 | list <- List.append list [(k, entry)] 19 | if List.length list > size then 20 | list <- List.tail list 21 | k) 22 | 23 | member this.get(key: uint64) : 'entry option = 24 | lock lockObj (fun () -> List.tryFind (fun (k, _) -> k = key) list |> Option.map snd) 25 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Lsp/Client.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer.Lsp 2 | 3 | open Ionide.LanguageServerProtocol 4 | open Ionide.LanguageServerProtocol.Server 5 | 6 | type CSharpLspClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) = 7 | inherit LspClient() 8 | 9 | override __.WindowShowMessage(p) = 10 | sendServerNotification "window/showMessage" (box p) |> Async.Ignore 11 | 12 | // TODO: Send notifications / requests to client only if client support it 13 | 14 | override __.WindowShowMessageRequest(p) = 15 | sendServerRequest.Send "window/showMessageRequest" (box p) 16 | 17 | override __.WindowLogMessage(p) = 18 | sendServerNotification "window/logMessage" (box p) |> Async.Ignore 19 | 20 | override __.TelemetryEvent(p) = 21 | sendServerNotification "telemetry/event" (box p) |> Async.Ignore 22 | 23 | override __.ClientRegisterCapability(p) = 24 | sendServerRequest.Send "client/registerCapability" (box p) 25 | 26 | override __.ClientUnregisterCapability(p) = 27 | sendServerRequest.Send "client/unregisterCapability" (box p) 28 | 29 | override __.WorkspaceWorkspaceFolders() = 30 | sendServerRequest.Send "workspace/workspaceFolders" () 31 | 32 | override __.WorkspaceConfiguration(p) = 33 | sendServerRequest.Send "workspace/configuration" (box p) 34 | 35 | override __.WorkspaceApplyEdit(p) = 36 | sendServerRequest.Send "workspace/applyEdit" (box p) 37 | 38 | override __.WorkspaceSemanticTokensRefresh() = 39 | sendServerNotification "workspace/semanticTokens/refresh" () 40 | 41 | override __.TextDocumentPublishDiagnostics(p) = 42 | sendServerNotification "textDocument/publishDiagnostics" (box p) |> Async.Ignore 43 | 44 | override __.WindowWorkDoneProgressCreate(createParams) = 45 | sendServerRequest.Send "window/workDoneProgress/create" (box createParams) 46 | 47 | override __.Progress(progressParams) = 48 | sendServerNotification "$/progress" (box progressParams) |> Async.Ignore 49 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Options.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer 2 | 3 | module Options = 4 | open Argu 5 | 6 | type CLIArguments = 7 | | [] Version 8 | | [] LogLevel of level:string 9 | | [] Solution of solution:string 10 | with 11 | interface IArgParserTemplate with 12 | member s.Usage = 13 | match s with 14 | | Version -> "display versioning information" 15 | | Solution _ -> ".sln file to load (relative to CWD)" 16 | | LogLevel _ -> "log level, ; default is `log`" 17 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Program.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Program 2 | 3 | open System 4 | open System.Reflection 5 | 6 | open Argu 7 | open Serilog 8 | open Serilog.Core 9 | open Serilog.Events 10 | 11 | open CSharpLanguageServer.Types 12 | open CSharpLanguageServer.Lsp 13 | 14 | [] 15 | let entry args = 16 | try 17 | let argParser = ArgumentParser.Create(programName = "csharp-ls") 18 | let serverArgs = argParser.Parse args 19 | 20 | serverArgs.TryGetResult(<@ Options.CLIArguments.Version @>) 21 | |> Option.iter (fun _ -> printfn "csharp-ls, %s" 22 | (Assembly.GetExecutingAssembly().GetName().Version |> string) 23 | exit 0) 24 | 25 | let logLevelArg = 26 | serverArgs.TryGetResult(<@ Options.CLIArguments.LogLevel @>) 27 | |> Option.defaultValue "log" 28 | 29 | let logLevel = 30 | match logLevelArg with 31 | | "error" -> LogEventLevel.Error 32 | | "warning" -> LogEventLevel.Warning 33 | | "info" -> LogEventLevel.Information 34 | | "log" -> LogEventLevel.Verbose 35 | | _ -> LogEventLevel.Information 36 | 37 | let logConfig = 38 | LoggerConfiguration() 39 | .MinimumLevel.ControlledBy(LoggingLevelSwitch(logLevel)) 40 | .Enrich.FromLogContext() 41 | .WriteTo.Async(fun conf -> 42 | conf.Console( 43 | outputTemplate = 44 | "[{Timestamp:HH:mm:ss.fff} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}", 45 | // Redirect all logs to stderr since stdout is used to communicate with client. 46 | standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), 47 | theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code 48 | ) 49 | |> ignore) 50 | 51 | Log.Logger <- logConfig.CreateLogger() 52 | 53 | let settings = { 54 | ServerSettings.Default with 55 | SolutionPath = serverArgs.TryGetResult(<@ Options.CLIArguments.Solution @>) 56 | LogLevel = logLevelArg 57 | } 58 | 59 | Server.start settings 60 | with 61 | | :? ArguParseException as ex -> 62 | printfn "%s" ex.Message 63 | 64 | match ex.ErrorCode with 65 | | ErrorCode.HelpText -> 0 66 | | _ -> 1 // Unrecognised arguments 67 | 68 | | e -> 69 | printfn "Server crashing error - %s \n %s" e.Message e.StackTrace 70 | 3 71 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/ProgressReporter.fs: -------------------------------------------------------------------------------- 1 | namespace CSharpLanguageServer 2 | 3 | open System 4 | open Ionide.LanguageServerProtocol 5 | open Ionide.LanguageServerProtocol.Server 6 | open Ionide.LanguageServerProtocol.Types 7 | 8 | type ProgressReporter(client: ILspClient) = 9 | let mutable canReport = false 10 | 11 | let mutable endSent = false 12 | 13 | member val Token = ProgressToken.C2 (Guid.NewGuid().ToString()) 14 | 15 | member this.Begin(title, ?cancellable, ?message, ?percentage) = async { 16 | let! progressCreateResult = client.WindowWorkDoneProgressCreate({ Token = this.Token }) 17 | 18 | match progressCreateResult with 19 | | Error _ -> 20 | canReport <- false 21 | | Ok() -> 22 | canReport <- true 23 | let param = WorkDoneProgressBegin.Create( 24 | title = title, 25 | ?cancellable = cancellable, 26 | ?message = message, 27 | ?percentage = percentage 28 | ) 29 | do! client.Progress({ Token = this.Token; Value = serialize param }) 30 | } 31 | 32 | member this.Report(?cancellable, ?message, ?percentage) = async { 33 | if canReport && not endSent then 34 | let param = WorkDoneProgressReport.Create( 35 | ?cancellable = cancellable, 36 | ?message = message, 37 | ?percentage = percentage 38 | ) 39 | do! client.Progress({ Token = this.Token; Value = serialize param }) 40 | } 41 | 42 | member this.End(?message) = async { 43 | if canReport && not endSent then 44 | endSent <- true 45 | let param = WorkDoneProgressEnd.Create( 46 | ?message = message 47 | ) 48 | do! client.Progress({ Token = this.Token; Value = serialize param }) 49 | } 50 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Types.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Types 2 | 3 | open Ionide.LanguageServerProtocol 4 | open Ionide.LanguageServerProtocol.Types 5 | open Ionide.LanguageServerProtocol.JsonRpc 6 | 7 | type ServerSettings = 8 | { SolutionPath: string option 9 | LogLevel: string 10 | ApplyFormattingOptions: bool 11 | } 12 | static member Default: ServerSettings = 13 | { SolutionPath = None 14 | LogLevel = "log" 15 | ApplyFormattingOptions = false 16 | } 17 | 18 | type CSharpMetadataInformation = 19 | { ProjectName: string 20 | AssemblyName: string 21 | SymbolName: string 22 | Source: string } 23 | 24 | type CSharpMetadataParams = 25 | { TextDocument: TextDocumentIdentifier } 26 | 27 | type CSharpMetadataResponse = CSharpMetadataInformation 28 | 29 | [] 30 | type ICSharpLspServer = 31 | inherit ILspServer 32 | abstract CSharpMetadata: CSharpMetadataParams -> AsyncLspResult 33 | 34 | [] 35 | type ICSharpLspClient = 36 | inherit ILspClient 37 | // Use a ClientCapabilitiesDTO instead of ClientCapabilities to avoid Option.map & Option.bind? 38 | // But ClientCapabilities is a complex type, write it again will be a huge work. 39 | abstract member Capabilities: ClientCapabilities option with get, set 40 | 41 | let defaultDocumentFilter: TextDocumentFilter = 42 | { Language = None 43 | Scheme = Some "file" 44 | Pattern = Some "**/*.cs" } 45 | 46 | // Type abbreviations cannot have augmentations, extensions 47 | let defaultDocumentSelector: DocumentSelector = 48 | [| 49 | defaultDocumentFilter |> U2.C1 50 | |] 51 | 52 | let emptyClientCapabilities: ClientCapabilities = 53 | { 54 | Workspace = None 55 | TextDocument = None 56 | NotebookDocument = None 57 | Window = None 58 | General = None 59 | Experimental = None 60 | } 61 | -------------------------------------------------------------------------------- /src/CSharpLanguageServer/Util.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Util 2 | 3 | open System 4 | open System.Runtime.InteropServices 5 | open System.IO 6 | 7 | let parseFileUri s: string = 8 | Uri(s).LocalPath 9 | 10 | let tryParseFileUri s: string option = 11 | try 12 | let uri = Uri(s) 13 | Some uri.LocalPath 14 | with _ex -> 15 | None 16 | 17 | let makeFileUri (path: string): string = 18 | let fullPath = Path.GetFullPath(path) 19 | 20 | match RuntimeInformation.IsOSPlatform(OSPlatform.Windows) with 21 | | true -> "file:///" + fullPath 22 | | false -> "file://" + fullPath 23 | 24 | let unwindProtect cleanupFn op = 25 | async { 26 | try 27 | return! op 28 | finally 29 | cleanupFn () 30 | } 31 | 32 | // TPL Task's wrap exceptions in AggregateException, -- this fn unpacks them 33 | let rec unpackException (exn : Exception) = 34 | match exn with 35 | | :? AggregateException as agg -> 36 | match Seq.tryExactlyOne agg.InnerExceptions with 37 | | Some x -> unpackException x 38 | | None -> exn 39 | | _ -> exn 40 | 41 | // flip f takes its (first) two arguments in the reverse order of f, just like 42 | // the function with the same name in Haskell. 43 | let flip f x y = f y x 44 | 45 | let curry f x y = f (x, y) 46 | let uncurry f (x, y) = f x y 47 | 48 | 49 | module Seq = 50 | let inline tryMaxBy (projection: 'T -> 'U) (source: 'T seq): 'T option = 51 | if isNull source || Seq.isEmpty source then 52 | None 53 | else 54 | Seq.maxBy projection source |> Some 55 | 56 | module Option = 57 | let inline ofString (value: string) = 58 | match String.IsNullOrWhiteSpace(value) with 59 | | true -> None 60 | | false -> Some value 61 | 62 | module Async = 63 | let map f computation = 64 | async.Bind(computation, f >> async.Return) 65 | 66 | module Map = 67 | let union map1 map2 = 68 | Map.fold (fun acc key value -> Map.add key value acc) map1 map2 69 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | 5 | false 6 | false 7 | FS0988 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/CodeActionTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.CodeActionTests 2 | 3 | open NUnit.Framework 4 | open Ionide.LanguageServerProtocol.Types 5 | 6 | open CSharpLanguageServer.Tests.Tooling 7 | 8 | [] 9 | let testCodeActionOnMethodNameWorks() = 10 | use client = setupServerClient defaultClientProfile 11 | "TestData/testCodeActionOnMethodNameWorks" 12 | client.StartAndWaitForSolutionLoad() 13 | 14 | use classFile = client.Open("Project/Class.cs") 15 | 16 | let caParams0: CodeActionParams = 17 | { TextDocument = { Uri = classFile.Uri } 18 | Range = { Start = { Line = 2u; Character = 16u } 19 | End = { Line = 2u; Character = 16u } 20 | } 21 | Context = { Diagnostics = [| |] 22 | Only = None 23 | TriggerKind = None 24 | } 25 | WorkDoneToken = None 26 | PartialResultToken = None 27 | } 28 | 29 | let caResult0 : TextDocumentCodeActionResult option = 30 | client.Request("textDocument/codeAction", caParams0) 31 | 32 | Assert.IsTrue(caResult0.IsSome) 33 | 34 | match caResult0 with 35 | | Some [| U2.C2 x |] -> 36 | Assert.AreEqual("Extract base class...", x.Title) 37 | Assert.AreEqual(None, x.Kind) 38 | Assert.AreEqual(None, x.Diagnostics) 39 | Assert.AreEqual(None, x.Disabled) 40 | Assert.IsTrue(x.Edit.IsSome) 41 | 42 | | _ -> failwith "Some [| U2.C1 x |] was expected" 43 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/DefinitionTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.DefinitionTests 2 | 3 | open NUnit.Framework 4 | open Ionide.LanguageServerProtocol.Types 5 | 6 | open CSharpLanguageServer.Tests.Tooling 7 | 8 | [] 9 | let testDefinitionWorks () = 10 | use client = setupServerClient defaultClientProfile 11 | "TestData/testDefinitionWorks" 12 | client.StartAndWaitForSolutionLoad() 13 | 14 | use classFile = client.Open("Project/Class.cs") 15 | 16 | let definitionParams0: DefinitionParams = 17 | { TextDocument = { Uri = classFile.Uri } 18 | Position = { Line = 0u; Character = 0u } 19 | WorkDoneToken = None 20 | PartialResultToken = None 21 | } 22 | 23 | let declaration0: Declaration option = 24 | client.Request("textDocument/definition", definitionParams0) 25 | 26 | Assert.IsTrue(declaration0.IsNone) 27 | 28 | let definitionParams1: DefinitionParams = 29 | { TextDocument = { Uri = classFile.Uri } 30 | Position = { Line = 2u; Character = 16u } 31 | WorkDoneToken = None 32 | PartialResultToken = None 33 | } 34 | 35 | let declaration1: Declaration option = 36 | client.Request("textDocument/definition", definitionParams1) 37 | 38 | match declaration1.Value with 39 | | U2.C1 _ -> failwith "Location[] was expected" 40 | | U2.C2 declaration1Locations -> 41 | let expectedLocations1: Location array = 42 | [| 43 | { Uri = classFile.Uri 44 | Range = { Start = { Line = 2u; Character = 16u } 45 | End = { Line = 2u; Character = 23u } } 46 | } 47 | |] 48 | 49 | Assert.AreEqual(expectedLocations1, declaration1Locations) 50 | 51 | 52 | [] 53 | let testDefinitionWorksInAspNetProject () = 54 | use client = setupServerClient defaultClientProfile 55 | "TestData/testDefinitionWorksInAspNetProject" 56 | client.StartAndWaitForSolutionLoad() 57 | 58 | use testIndexViewModelCsFile = client.Open("Project/Models/Test/IndexViewModel.cs") 59 | use testControllerCsFile = client.Open("Project/Controllers/TestController.cs") 60 | 61 | let definitionParams0: DefinitionParams = 62 | { TextDocument = { Uri = testControllerCsFile.Uri } 63 | Position = { Line = 11u; Character = 12u } 64 | WorkDoneToken = None 65 | PartialResultToken = None 66 | } 67 | 68 | let definition0: Declaration option = 69 | client.Request("textDocument/definition", definitionParams0) 70 | 71 | let expectedLocations0: Location array = 72 | [| 73 | { Uri = testIndexViewModelCsFile.Uri 74 | Range = { Start = { Line = 3u; Character = 19u } 75 | End = { Line = 3u; Character = 25u } } 76 | } 77 | |] 78 | 79 | match definition0 with 80 | | Some (U2.C2 definition0Locations) -> 81 | Assert.AreEqual(expectedLocations0, definition0Locations) 82 | | _ -> failwithf "Some Location[] was expected but %s received" (string definition0) 83 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/DiagnosticTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.DiagnosticTests 2 | 3 | open System.Threading 4 | 5 | open NUnit.Framework 6 | open Ionide.LanguageServerProtocol.Types 7 | 8 | open CSharpLanguageServer.Tests.Tooling 9 | 10 | [] 11 | let testPushDiagnosticsWork () = 12 | use client = setupServerClient defaultClientProfile 13 | "TestData/testPushDiagnosticsWork" 14 | client.StartAndWaitForSolutionLoad() 15 | 16 | // 17 | // open Class.cs file and wait for diagnostics to be pushed 18 | // 19 | use classFile = client.Open("Project/Class.cs") 20 | 21 | Thread.Sleep(4000) 22 | 23 | let state = client.GetState() 24 | let version0, diagnosticList0 = state.PushDiagnostics |> Map.find classFile.Uri 25 | 26 | Assert.AreEqual(None, version0) 27 | 28 | Assert.AreEqual(3, diagnosticList0.Length) 29 | 30 | let diagnostic0 = diagnosticList0.[0] 31 | Assert.AreEqual("Identifier expected", diagnostic0.Message) 32 | Assert.AreEqual(Some DiagnosticSeverity.Error, diagnostic0.Severity) 33 | Assert.AreEqual(0, diagnostic0.Range.Start.Line) 34 | Assert.AreEqual(3, diagnostic0.Range.Start.Character) 35 | 36 | let diagnostic1 = diagnosticList0.[1] 37 | Assert.AreEqual("; expected", diagnostic1.Message) 38 | 39 | let diagnostic2 = diagnosticList0.[2] 40 | Assert.AreEqual( 41 | "The type or namespace name 'XXX' could not be found (are you missing a using directive or an assembly reference?)", 42 | diagnostic2.Message) 43 | 44 | // 45 | // now change the file to contain no content (and thus no diagnostics) 46 | // 47 | classFile.DidChange("") 48 | 49 | Thread.Sleep(4000) 50 | 51 | let state = client.GetState() 52 | let version1, diagnosticList1 = state.PushDiagnostics |> Map.find classFile.Uri 53 | 54 | Assert.AreEqual(None, version1) 55 | 56 | Assert.AreEqual(0, diagnosticList1.Length) 57 | () 58 | 59 | 60 | [] 61 | let testPullDiagnosticsWork () = 62 | use client = setupServerClient defaultClientProfile 63 | "TestData/testPullDiagnosticsWork" 64 | client.StartAndWaitForSolutionLoad() 65 | 66 | // 67 | // open Class.cs file and pull diagnostics 68 | // 69 | use classFile = client.Open("Project/Class.cs") 70 | 71 | let diagnosticParams: DocumentDiagnosticParams = 72 | { WorkDoneToken = None 73 | PartialResultToken = None 74 | TextDocument = { Uri = classFile.Uri } 75 | Identifier = None 76 | PreviousResultId = None } 77 | 78 | let report0: DocumentDiagnosticReport option = 79 | client.Request("textDocument/diagnostic", diagnosticParams) 80 | 81 | match report0 with 82 | | Some (U2.C1 report) -> 83 | Assert.AreEqual("full", report.Kind) 84 | Assert.AreEqual(None, report.ResultId) 85 | Assert.AreEqual(3, report.Items.Length) 86 | 87 | let diagnostic0 = report.Items.[0] 88 | Assert.AreEqual(0, diagnostic0.Range.Start.Line) 89 | Assert.AreEqual(3, diagnostic0.Range.Start.Character) 90 | Assert.AreEqual(Some DiagnosticSeverity.Error, diagnostic0.Severity) 91 | Assert.AreEqual("Identifier expected", diagnostic0.Message) 92 | Assert.AreEqual( 93 | "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS1001)", 94 | diagnostic0.CodeDescription.Value.Href) 95 | 96 | let diagnostic1 = report.Items.[1] 97 | Assert.AreEqual("; expected", diagnostic1.Message) 98 | 99 | let diagnostic2 = report.Items.[2] 100 | Assert.AreEqual( 101 | "The type or namespace name 'XXX' could not be found (are you missing a using directive or an assembly reference?)", 102 | diagnostic2.Message) 103 | | _ -> failwith "U2.C1 is expected" 104 | 105 | // 106 | // now try to do the same but with file fixed to contain no content (and thus no diagnostics) 107 | // 108 | classFile.DidChange("") 109 | 110 | let report1: DocumentDiagnosticReport option = 111 | client.Request("textDocument/diagnostic", diagnosticParams) 112 | 113 | match report1 with 114 | | Some (U2.C1 report) -> 115 | Assert.AreEqual("full", report.Kind) 116 | Assert.AreEqual(None, report.ResultId) 117 | Assert.AreEqual(0, report.Items.Length) 118 | | _ -> failwith "U2.C1 is expected" 119 | () 120 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.DocumentFormattingTests 2 | 3 | open System.IO 4 | 5 | open NUnit.Framework 6 | open Ionide.LanguageServerProtocol.Types 7 | 8 | open CSharpLanguageServer.Tests.Tooling 9 | 10 | [] 11 | let testEditorConfigFormatting () = 12 | use client = setupServerClient defaultClientProfile 13 | "TestData/testEditorConfigFormatting" 14 | client.StartAndWaitForSolutionLoad() 15 | 16 | use classFile = client.Open("Project/Class.cs") 17 | 18 | let docFormattingParams0: DocumentFormattingParams = 19 | { TextDocument = { Uri = classFile.Uri } 20 | WorkDoneToken = None 21 | Options = { 22 | TabSize = 8u 23 | InsertSpaces = false 24 | TrimTrailingWhitespace = Some true 25 | InsertFinalNewline = Some true 26 | TrimFinalNewlines = Some true 27 | } 28 | } 29 | 30 | let textEdits: TextEdit[] option = 31 | client.Request("textDocument/formatting", docFormattingParams0) 32 | 33 | match textEdits with 34 | | Some tes -> 35 | let expectedClassContents = File.ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) 36 | Assert.AreEqual(expectedClassContents, classFile.GetFileContentsWithTextEditsApplied(tes)) 37 | | None -> failwith "Some TextEdit's were expected" 38 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/DocumentationTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.DocumentationTests 2 | 3 | open System 4 | 5 | open NUnit.Framework 6 | 7 | open CSharpLanguageServer.DocumentationUtil 8 | 9 | [] 12 | [doc string", 14 | "doc string")>] 15 | [\ndoc string\n\n", 17 | "doc string")>] 18 | [doc string\n y", 20 | """doc string 21 | 22 | Parameters: 23 | - ``x``: y""")>] 24 | [Gets the standard error output stream.\n\ 27 | A that represents the standard error output stream.\n\ 28 | ", 29 | """Gets the standard error output stream. 30 | 31 | Returns: A ``System.IO.TextWriter`` that represents the standard error output stream.""" 32 | )>] 33 | [\n\ 36 | Asserts that a condition is true. If the condition is false the method throws\n\ 37 | an .\n\ 38 | \n\ 39 | The evaluated condition\n\ 40 | \n\ 41 | ", 42 | """Asserts that a condition is true. If the condition is false the method throws an ``NUnit.Framework.AssertionException``. 43 | 44 | Parameters: 45 | - ``condition``: The evaluated condition""" 46 | )>] 47 | [Writes a string to the text stream, followed by a line terminator.\n\ 50 | The string to write. If is , only the line terminator is written.\n\ 51 | The is closed.\n\ 52 | An I/O error occurs.\n\ 53 | ", 54 | """Writes a string to the text stream, followed by a line terminator. 55 | 56 | Parameters: 57 | - ``value``: The string to write. If ``value`` is ``null``, only the line terminator is written. 58 | 59 | Exceptions: 60 | - ``System.ObjectDisposedException``: The ``System.IO.TextWriter`` is closed. 61 | - ``System.IO.IOException``: An I/O error occurs.""" 62 | )>] 63 | [ 65 | 66 | Test method. 67 | Does another thing. 68 | 69 | 70 | """, 71 | "Test method. Does another thing.")>] 72 | [test xx", 74 | "test ``xx``")>] 75 | [test contents-of-unknown-tag", 77 | "test contents-of-unknown-tag")>] 78 | [test contents-of-unknown-inline-tag", 80 | "test contents-of-unknown-inline-tag")>] 81 | [summary\n 83 | contents-of-unknown-top-level-tag", 84 | "summary\n\ 85 | contents-of-unknown-top-level-tag")>] 86 | [summaryremarks", 88 | "summary\n\nRemarks: remarks")>] 89 | [A", 91 | "A")>] 92 | [yb", 94 | """ 95 | Parameters: 96 | - ``x``: y 97 | - ``a``: b""")>] 98 | [descy", 100 | """desc 101 | 102 | Types: 103 | - ``x``: y""")>] 104 | [ 106 | 107 | Adds a child . Nodes can have any number of children, but every child must have a unique name. Child nodes are automatically deleted when the parent node is deleted, so an entire scene can be removed by deleting its topmost node. 108 | If is , improves the readability of the added . If not named, the is renamed to its type, and if it shares with a sibling, a number is suffixed more appropriately. This operation is very slow. As such, it is recommended leaving this to , which assigns a dummy name featuring @ in both situations. 109 | If is different than , the child will be added as internal node. These nodes are ignored by methods like , unless their parameter include_internal is . The intended usage is to hide the internal nodes from the user, so the user won't accidentally delete or modify them. Used by some GUI nodes, e.g. . See for available modes. 110 | Note: If already has a parent, this method will fail. Use first to remove from its current parent. For example: 111 | 112 | Node childNode = GetChild(0); 113 | if (childNode.GetParent() != null) 114 | { 115 | childNode.GetParent().RemoveChild(childNode); 116 | } 117 | AddChild(childNode); 118 | If you need the child node to be added below a specific node in the list of children, use instead of this method. 119 | Note: If you want a child to be persisted to a , you must set in addition to calling . This is typically relevant for tool scripts and editor plugins. If is called without setting , the newly added will not be visible in the scene tree, though it will be visible in the 2D/3D view. 120 | 121 | 122 | """, 123 | """Adds a child ``node``. Nodes can have any number of children, but every child must have a unique name. Child nodes are automatically deleted when the parent node is deleted, so an entire scene can be removed by deleting its topmost node. 124 | 125 | If ``forceReadableName`` is ``true``, improves the readability of the added ``node``. If not named, the ``node`` is renamed to its type, and if it shares ``Godot.Node.Name`` with a sibling, a number is suffixed more appropriately. This operation is very slow. As such, it is recommended leaving this to ``false``, which assigns a dummy name featuring ``@`` in both situations. 126 | 127 | If ``internal`` is different than ``Godot.Node.InternalMode.Disabled``, the child will be added as internal node. These nodes are ignored by methods like ``Godot.Node.GetChildren(System.Boolean)``, unless their parameter ``include_internal`` is ``true``. The intended usage is to hide the internal nodes from the user, so the user won't accidentally delete or modify them. Used by some GUI nodes, e.g. ``Godot.ColorPicker``. See ``Godot.Node.InternalMode`` for available modes. 128 | 129 | Note: If ``node`` already has a parent, this method will fail. Use ``Godot.Node.RemoveChild(Godot.Node)`` first to remove ``node`` from its current parent. For example: 130 | 131 | 132 | Node childNode = GetChild(0); 133 | if (childNode.GetParent() != null) 134 | { 135 | childNode.GetParent().RemoveChild(childNode); 136 | } 137 | AddChild(childNode); 138 | 139 | If you need the child node to be added below a specific node in the list of children, use ``Godot.Node.AddSibling(Godot.Node,System.Boolean)`` instead of this method. 140 | 141 | Note: If you want a child to be persisted to a ``Godot.PackedScene``, you must set ``Godot.Node.Owner`` in addition to calling ``Godot.Node.AddChild(Godot.Node,System.Boolean,Godot.Node.InternalMode)``. This is typically relevant for tool scripts and editor plugins. If ``Godot.Node.AddChild(Godot.Node,System.Boolean,Godot.Node.InternalMode)`` is called without setting ``Godot.Node.Owner``, the newly added ``Godot.Node`` will not be visible in the scene tree, though it will be visible in the 2D/3D view.""" 142 | )>] 143 | let testFormatDocXml (inputXml, expectedMD: string) = 144 | let resultMd = String.Join("\n", formatDocXml inputXml) 145 | Assert.AreEqual(expectedMD.Replace("\r\n", "\n"), resultMd) 146 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/HoverTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.HoverTests 2 | 3 | open NUnit.Framework 4 | open Ionide.LanguageServerProtocol.Types 5 | 6 | open CSharpLanguageServer.Tests.Tooling 7 | 8 | [] 9 | let testHoverWorks() = 10 | use client = setupServerClient defaultClientProfile 11 | "TestData/testHoverWorks" 12 | client.StartAndWaitForSolutionLoad() 13 | 14 | use classFile = client.Open("Project/Class.cs") 15 | 16 | // 17 | // check hover at method name 18 | // 19 | let hover0Params: HoverParams = 20 | { TextDocument = { Uri = classFile.Uri } 21 | Position = { Line = 2u; Character = 16u } 22 | WorkDoneToken = None 23 | } 24 | 25 | let hover0: Hover option = 26 | client.Request("textDocument/hover", hover0Params) 27 | 28 | Assert.IsTrue(hover0.IsSome) 29 | 30 | match hover0 with 31 | | Some hover -> 32 | match hover.Contents with 33 | | U3.C1 c -> 34 | Assert.AreEqual(MarkupKind.Markdown, c.Kind) 35 | Assert.AreEqual("```csharp\nvoid Class.Method(string arg)\n```", c.Value) 36 | | _ -> failwith "C1 was expected" 37 | 38 | Assert.IsTrue(hover.Range.IsNone) 39 | 40 | | _ -> failwith "Some (U3.C1 c) was expected" 41 | 42 | // 43 | // check hover on `string` value (external System.String type) 44 | // 45 | let hover1Params: HoverParams = 46 | { TextDocument = { Uri = classFile.Uri } 47 | Position = { Line = 4u; Character = 8u } 48 | WorkDoneToken = None 49 | } 50 | 51 | let hover1: Hover option = 52 | client.Request("textDocument/hover", hover1Params) 53 | 54 | Assert.IsTrue(hover1.IsSome) 55 | 56 | match hover1 with 57 | | Some hover -> 58 | match hover.Contents with 59 | | U3.C1 c -> 60 | Assert.AreEqual(MarkupKind.Markdown, c.Kind) 61 | Assert.AreEqual("""```csharp 62 | string 63 | ``` 64 | 65 | Represents text as a sequence of UTF-16 code units.""", c.Value.ReplaceLineEndings()) 66 | | _ -> failwith "C1 was expected" 67 | 68 | Assert.IsTrue(hover.Range.IsNone) 69 | 70 | | _ -> failwith "Some (U3.C1 c) was expected" 71 | 72 | // 73 | // check hover at beginning of the file (nothing should come up) 74 | // 75 | let hover2Params: HoverParams = 76 | { TextDocument = { Uri = classFile.Uri } 77 | Position = { Line = 0u; Character = 0u } 78 | WorkDoneToken = None 79 | } 80 | 81 | let hover2: Hover option = 82 | client.Request("textDocument/hover", hover2Params) 83 | 84 | Assert.IsTrue(hover2.IsNone) 85 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/InitializationTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.InitializationTests 2 | 3 | open NUnit.Framework 4 | 5 | open CSharpLanguageServer.Tests.Tooling 6 | open Ionide.LanguageServerProtocol.Types 7 | 8 | let assertHoverWorks (client: ClientController) file pos expectedMarkupContent = 9 | use classFile = client.Open(file) 10 | 11 | let hover0Params: HoverParams = 12 | { TextDocument = { Uri = classFile.Uri } 13 | Position = pos 14 | WorkDoneToken = None 15 | } 16 | 17 | let hover0: Hover option = client.Request("textDocument/hover", hover0Params) 18 | 19 | match hover0 with 20 | | Some { Contents = U3.C1 markupContent; Range = None } -> 21 | Assert.AreEqual(MarkupKind.Markdown, markupContent.Kind) 22 | Assert.AreEqual(expectedMarkupContent, markupContent.Value) 23 | 24 | | x -> failwithf "'{ Contents = U3.C1 markupContent; Range = None }' was expected but '%s' received" (string x) 25 | 26 | 27 | [] 28 | let testServerRegistersCapabilitiesWithTheClient () = 29 | use client = setupServerClient defaultClientProfile 30 | "TestData/testServerRegistersCapabilitiesWithTheClient" 31 | client.StartAndWaitForSolutionLoad() 32 | 33 | let serverInfo = client.GetState().ServerInfo.Value 34 | Assert.AreEqual("csharp-ls", serverInfo.Name) 35 | 36 | let serverCaps = client.GetState().ServerCapabilities.Value 37 | 38 | Assert.AreEqual( 39 | { Change = Some TextDocumentSyncKind.Incremental 40 | OpenClose = Some true 41 | Save = Some (U2.C2 { IncludeText = Some true }) 42 | WillSave = None 43 | WillSaveWaitUntil = None 44 | } 45 | |> U2.C1 |> Some, 46 | serverCaps.TextDocumentSync) 47 | 48 | Assert.AreEqual( 49 | null, 50 | serverCaps.Workspace) 51 | 52 | Assert.AreEqual( 53 | true |> U2.C1 |> Some, 54 | serverCaps.HoverProvider) 55 | 56 | Assert.AreEqual( 57 | true |> U3.C1 |> Some, 58 | serverCaps.ImplementationProvider) 59 | 60 | Assert.AreEqual( 61 | true |> U2.C1 |> Some, 62 | serverCaps.DocumentSymbolProvider) 63 | 64 | Assert.AreEqual( 65 | true |> U2.C1 |> Some, 66 | serverCaps.DefinitionProvider) 67 | 68 | Assert.AreEqual( 69 | null, 70 | serverCaps.InlineValueProvider) 71 | 72 | Assert.AreEqual( 73 | { DocumentSelector = Some [|U2.C1 { Language = None 74 | Scheme = Some "file" 75 | Pattern = Some "**/*.cs" }|] 76 | WorkDoneProgress = None 77 | Identifier = None 78 | InterFileDependencies = false 79 | WorkspaceDiagnostics = false 80 | Id = None 81 | } 82 | |> U2.C2 |> Some, 83 | serverCaps.DiagnosticProvider) 84 | 85 | Assert.AreEqual( 86 | true |> U2.C1 |> Some, 87 | serverCaps.DocumentHighlightProvider) 88 | 89 | Assert.AreEqual( 90 | { WorkDoneProgress = None 91 | TriggerCharacters = Some [|"."; "'"|] 92 | AllCommitCharacters = None 93 | ResolveProvider = None 94 | CompletionItem = None } 95 | |> Some, 96 | serverCaps.CompletionProvider) 97 | 98 | Assert.AreEqual( 99 | { WorkDoneProgress = None 100 | CodeActionKinds = None 101 | ResolveProvider = Some true } 102 | |> U2.C2 |> Some, 103 | serverCaps.CodeActionProvider) 104 | 105 | Assert.AreEqual( 106 | true |> U2.C1 |> Some, 107 | serverCaps.RenameProvider) 108 | 109 | Assert.AreEqual( 110 | null, 111 | serverCaps.DeclarationProvider) 112 | 113 | Assert.AreEqual( 114 | true |> U2.C1 |> Some, 115 | serverCaps.DocumentFormattingProvider) 116 | 117 | Assert.AreEqual( 118 | true |> U2.C1 |> Some, 119 | serverCaps.ReferencesProvider) 120 | 121 | Assert.AreEqual( 122 | true |> U2.C1 |> Some, 123 | serverCaps.WorkspaceSymbolProvider) 124 | 125 | Assert.AreEqual( 126 | { WorkDoneProgress = None 127 | TriggerCharacters = Some [|"("; ","; "<"; "{"; "["|] 128 | RetriggerCharacters = None } 129 | |> Some, 130 | serverCaps.SignatureHelpProvider) 131 | 132 | Assert.AreEqual( 133 | null, 134 | serverCaps.MonikerProvider) 135 | 136 | Assert.IsTrue(client.ServerDidRespondTo "initialize") 137 | Assert.IsTrue(client.ServerDidRespondTo "initialized") 138 | 139 | 140 | [] 141 | let testSlnxSolutionFileWillBeFoundAndLoaded () = 142 | use client = setupServerClient defaultClientProfile "TestData/testSlnx" 143 | client.StartAndWaitForSolutionLoad() 144 | 145 | Assert.IsTrue(client.ServerMessageLogContains(fun m -> m.Contains "1 solution(s) found")) 146 | 147 | Assert.IsTrue(client.ServerDidRespondTo "initialize") 148 | Assert.IsTrue(client.ServerDidRespondTo "initialized") 149 | 150 | assertHoverWorks 151 | client 152 | "Project/Class.cs" { Line = 2u; Character = 16u } 153 | "```csharp\nvoid Class.MethodA(string arg)\n```" 154 | 155 | 156 | [] 157 | let testMultiTargetProjectLoads () = 158 | use client = setupServerClient defaultClientProfile "TestData/testMultiTargetProjectLoads" 159 | client.StartAndWaitForSolutionLoad() 160 | 161 | Assert.IsTrue(client.ServerMessageLogContains(fun m -> m.Contains "loading project")) 162 | 163 | assertHoverWorks 164 | client 165 | "Project/Class.cs" { Line = 2u; Character = 16u } 166 | "```csharp\nvoid Class.Method(string arg)\n```" 167 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/InternalTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.InternalTests 2 | 3 | open NUnit.Framework 4 | 5 | open CSharpLanguageServer.RoslynHelpers 6 | 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | let testTheMostCapableTfmIsSelected(tfmList: string, expectedTfm: string) = 14 | let selectedTfm = tfmList.Split(";") |> selectMostCapableCompatibleTfm 15 | Assert.AreEqual(expectedTfm |> Option.ofObj, selectedTfm) 16 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/ReferenceTests.fs: -------------------------------------------------------------------------------- 1 | module CSharpLanguageServer.Tests.ReferenceTests 2 | 3 | open NUnit.Framework 4 | open Ionide.LanguageServerProtocol.Types 5 | 6 | open CSharpLanguageServer.Tests.Tooling 7 | 8 | [] 9 | [] 10 | let testReferenceWorks(testDataDir: string) = 11 | use client = setupServerClient defaultClientProfile testDataDir 12 | client.StartAndWaitForSolutionLoad() 13 | 14 | use classFile = client.Open("Project/Class.cs") 15 | 16 | // 17 | // try references request at empty line line 1 -- should return 0 results 18 | // 19 | let referenceParams0: ReferenceParams = 20 | { TextDocument = { Uri = classFile.Uri } 21 | Position = { Line = 0u; Character = 0u } 22 | WorkDoneToken = None 23 | PartialResultToken = None 24 | Context = { IncludeDeclaration = false } 25 | } 26 | 27 | let locations0: Location[] option = 28 | client.Request("textDocument/references", referenceParams0) 29 | 30 | Assert.IsTrue(locations0.IsNone) 31 | 32 | // 33 | // try references request at MethodA declaration on line 2 34 | // 35 | let referenceParams1: ReferenceParams = 36 | { TextDocument = { Uri = classFile.Uri } 37 | Position = { Line = 2u; Character = 16u } 38 | WorkDoneToken = None 39 | PartialResultToken = None 40 | Context = { IncludeDeclaration = false } 41 | } 42 | 43 | let locations1: Location[] option = 44 | client.Request("textDocument/references", referenceParams1) 45 | 46 | let expectedLocations1: Location array = 47 | [| 48 | { Uri = classFile.Uri 49 | Range = { 50 | Start = { Line = 8u; Character = 8u } 51 | End = { Line = 8u; Character = 15u } 52 | } 53 | } 54 | |] 55 | 56 | Assert.AreEqual(expectedLocations1, locations1.Value) 57 | 58 | // 59 | // try references request at MethodA declaration on line 2 60 | // (with IncludeDeclaration=true) 61 | // 62 | let referenceParams2: ReferenceParams = 63 | { TextDocument = { Uri = classFile.Uri } 64 | Position = { Line = 2u; Character = 16u } 65 | WorkDoneToken = None 66 | PartialResultToken = None 67 | Context = { IncludeDeclaration = true } 68 | } 69 | 70 | let locations2: Location[] option = 71 | client.Request("textDocument/references", referenceParams2) 72 | 73 | let expectedLocations2: Location array = 74 | [| 75 | { Uri = classFile.Uri 76 | Range = { 77 | Start = { Line = 2u; Character = 16u } 78 | End = { Line = 2u; Character = 23u } 79 | } 80 | } 81 | 82 | { Uri = classFile.Uri 83 | Range = { 84 | Start = { Line = 8u; Character = 8u } 85 | End = { Line = 8u; Character = 15u } 86 | } 87 | } 88 | |] 89 | 90 | Assert.AreEqual(expectedLocations2, locations2.Value) 91 | 92 | [] 93 | [] 94 | let testReferenceWorksToAspNetRazorPageReferencedValue() = 95 | use client = setupServerClient defaultClientProfile 96 | "TestData/testReferenceWorksToAspNetRazorPageReferencedValue" 97 | client.StartAndWaitForSolutionLoad() 98 | 99 | use testIndexViewModelCsFile = client.Open("Project/Models/Test/IndexViewModel.cs") 100 | use testControllerCsFile = client.Open("Project/Controllers/TestController.cs") 101 | use viewsTestIndexCshtmlFile = client.Open("Project/Views/Test/Index.cshtml") 102 | 103 | let referenceParams0: ReferenceParams = 104 | { TextDocument = { Uri = testIndexViewModelCsFile.Uri } 105 | Position = { Line = 3u; Character = 20u } 106 | WorkDoneToken = None 107 | PartialResultToken = None 108 | Context = { IncludeDeclaration = false } 109 | } 110 | 111 | let locations0: Location[] option = 112 | client.Request("textDocument/references", referenceParams0) 113 | 114 | Assert.IsTrue(locations0.IsSome) 115 | Assert.AreEqual(2, locations0.Value.Length) 116 | 117 | let expectedLocations0: Location array = 118 | [| 119 | { Uri = testControllerCsFile.Uri 120 | Range = { Start = { Line = 11u; Character = 12u } 121 | End = { Line = 11u; Character = 18u } } 122 | } 123 | 124 | { Uri = viewsTestIndexCshtmlFile.Uri 125 | Range = { Start = { Line = 1u; Character = 7u } 126 | End = { Line = 1u; Character = 13u } } 127 | } 128 | |] 129 | 130 | Assert.AreEqual(expectedLocations0, locations0.Value) 131 | 132 | // 133 | // do same but with IncludeDeclaration=true 134 | // 135 | let referenceParams1: ReferenceParams = 136 | { TextDocument = { Uri = testIndexViewModelCsFile.Uri } 137 | Position = { Line = 3u; Character = 20u } 138 | WorkDoneToken = None 139 | PartialResultToken = None 140 | Context = { IncludeDeclaration = true } 141 | } 142 | 143 | let locations1: Location[] option = 144 | client.Request("textDocument/references", referenceParams1) 145 | 146 | Assert.IsTrue(locations1.IsSome) 147 | Assert.AreEqual(5, locations1.Value.Length) 148 | 149 | let expectedLocations1: Location array = 150 | [| 151 | { Uri = viewsTestIndexCshtmlFile.Uri 152 | Range = { Start = { Line = 1u; Character = 7u } 153 | End = { Line = 1u; Character = 13u } } 154 | } 155 | 156 | { Uri = testIndexViewModelCsFile.Uri 157 | Range = { Start = { Line = 3u; Character = 19u } 158 | End = { Line = 3u; Character = 25u } } 159 | } 160 | 161 | { Uri = testIndexViewModelCsFile.Uri 162 | Range = { Start = { Line = 3u; Character = 28u } 163 | End = { Line = 3u; Character = 31u } } 164 | } 165 | 166 | { Uri = testIndexViewModelCsFile.Uri 167 | Range = { Start = { Line = 3u; Character = 33u } 168 | End = { Line = 3u; Character = 36u } } 169 | } 170 | 171 | { Uri = testControllerCsFile.Uri 172 | Range = { Start = { Line = 11u; Character = 12u } 173 | End = { Line = 11u; Character = 18u } } 174 | } 175 | |] 176 | 177 | let sortedLocations1 = 178 | locations1.Value 179 | |> Array.sortBy (fun f -> (f.Range.Start.Line, f.Range.Start.Character)) 180 | 181 | Assert.AreEqual(expectedLocations1, sortedLocations1) 182 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testCodeActionOnMethodNameWorks/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void Method(string arg) 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testCodeActionOnMethodNameWorks/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorks/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void MethodA(string arg) 4 | { 5 | } 6 | 7 | public void MethodB(string arg) 8 | { 9 | MethodA(arg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorks/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Project.Models.Test; 3 | 4 | namespace Printlog.Web.ClientPart.Controllers; 5 | 6 | public class TestController : Controller 7 | { 8 | public IActionResult Index() 9 | { 10 | var model = new IndexViewModel() 11 | { 12 | Output = "test" 13 | }; 14 | 15 | return View(model); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Models/Test/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Project.Models.Test; 2 | public class IndexViewModel 3 | { 4 | public string? Output { get; set; } 5 | } 6 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | namespace Project; 6 | 7 | public class Program 8 | { 9 | public static async Task Main(string[] args) 10 | { 11 | await BuildWebHost(args).RunAsync(); 12 | } 13 | 14 | public static IWebHost BuildWebHost(string[] args) 15 | { 16 | var builder = WebHost.CreateDefaultBuilder(args); 17 | builder = builder.UseKestrel().UseStartup(); 18 | return builder.Build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Project; 7 | 8 | public class Startup 9 | { 10 | public Startup(IConfiguration configuration, IWebHostEnvironment env) 11 | { 12 | } 13 | 14 | public void ConfigureServices(IServiceCollection services) 15 | { 16 | services.AddOptions(); 17 | } 18 | 19 | public void Configure( 20 | IApplicationBuilder app, 21 | IWebHostEnvironment env) 22 | { 23 | app.UseAuthentication(); 24 | app.UseAuthorization(); 25 | 26 | app.UseEndpoints(endpoints => { 27 | endpoints.MapControllers(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Views/Test/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model Project.Models.Test.IndexViewModel 2 | @Model.Output 3 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 2 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testDefinitionWorksInAspNetProject/Project/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testEditorConfigFormatting/Project/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | csharp_space_after_keywords_in_control_flow_statements = false 5 | csharp_space_before_open_square_brackets = true 6 | csharp_space_after_cast = true 7 | csharp_new_line_before_open_brace = none 8 | csharp_indent_switch_labels = false 9 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testEditorConfigFormatting/Project/Class.cs: -------------------------------------------------------------------------------- 1 | // csharp_space_after_keywords_in_control_flow_statements = false 2 | if (true) ; 3 | 4 | // csharp_space_before_open_square_brackets = true 5 | var array = new int[10]; 6 | 7 | // csharp_space_after_cast = true 8 | var num = (int)15.0; 9 | 10 | // csharp_new_line_before_open_brace = none 11 | void foo() 12 | { 13 | 14 | } 15 | 16 | // csharp_indent_switch_labels = true 17 | switch (Random.Shared.Next(3)) 18 | { 19 | case 1: 20 | Console.WriteLine("Case 1"); 21 | break; 22 | case 2: 23 | Console.WriteLine("Case 2"); 24 | break; 25 | default: 26 | Console.WriteLine("Default case"); 27 | break; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testEditorConfigFormatting/Project/ExpectedFormatting.cs.txt: -------------------------------------------------------------------------------- 1 | // csharp_space_after_keywords_in_control_flow_statements = false 2 | if(true) ; 3 | 4 | // csharp_space_before_open_square_brackets = true 5 | var array = new int [10]; 6 | 7 | // csharp_space_after_cast = true 8 | var num = (int) 15.0; 9 | 10 | // csharp_new_line_before_open_brace = none 11 | void foo() { 12 | 13 | } 14 | 15 | // csharp_indent_switch_labels = true 16 | switch(Random.Shared.Next(3)) { 17 | case 1: 18 | Console.WriteLine("Case 1"); 19 | break; 20 | case 2: 21 | Console.WriteLine("Case 2"); 22 | break; 23 | default: 24 | Console.WriteLine("Default case"); 25 | break; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testEditorConfigFormatting/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testHoverWorks/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void Method(string arg) 4 | { 5 | string str = ""; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testHoverWorks/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void Method(string arg) 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testMultiTargetProjectLoads/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0;net8.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testPullDiagnosticsWork/Project/Class.cs: -------------------------------------------------------------------------------- 1 | XXX 2 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testPullDiagnosticsWork/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testPushDiagnosticsWork/Project/Class.cs: -------------------------------------------------------------------------------- 1 | XXX 2 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testPushDiagnosticsWork/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksDotnet8/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void MethodA(string arg) 4 | { 5 | } 6 | 7 | public void MethodB(string arg) 8 | { 9 | MethodA(arg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksDotnet8/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksDotnet9/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void MethodA(string arg) 4 | { 5 | } 6 | 7 | public void MethodB(string arg) 8 | { 9 | MethodA(arg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksDotnet9/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Project.Models.Test; 3 | 4 | namespace Printlog.Web.ClientPart.Controllers; 5 | 6 | public class TestController : Controller 7 | { 8 | public IActionResult Index() 9 | { 10 | var model = new IndexViewModel() 11 | { 12 | Output = "test" 13 | }; 14 | 15 | return View(model); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Project.Models.Test; 2 | public class IndexViewModel 3 | { 4 | public string? Output { get; set; } 5 | } 6 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | namespace Project; 6 | 7 | public class Program 8 | { 9 | public static async Task Main(string[] args) 10 | { 11 | await BuildWebHost(args).RunAsync(); 12 | } 13 | 14 | public static IWebHost BuildWebHost(string[] args) 15 | { 16 | var builder = WebHost.CreateDefaultBuilder(args); 17 | builder = builder.UseKestrel().UseStartup(); 18 | return builder.Build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Project; 7 | 8 | public class Startup 9 | { 10 | public Startup(IConfiguration configuration, IWebHostEnvironment env) 11 | { 12 | } 13 | 14 | public void ConfigureServices(IServiceCollection services) 15 | { 16 | services.AddOptions(); 17 | } 18 | 19 | public void Configure( 20 | IApplicationBuilder app, 21 | IWebHostEnvironment env) 22 | { 23 | app.UseAuthentication(); 24 | app.UseAuthorization(); 25 | 26 | app.UseEndpoints(endpoints => { 27 | endpoints.MapControllers(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Views/Test/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model Project.Models.Test.IndexViewModel 2 | @Model.Output 3 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 2 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToAspNetRazorPageReferencedValue/Project/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testServerRegistersCapabilitiesWithTheClient/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class {} 2 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testServerRegistersCapabilitiesWithTheClient/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testSlnx/Project/Class.cs: -------------------------------------------------------------------------------- 1 | class Class 2 | { 3 | public void MethodA(string arg) 4 | { 5 | } 6 | 7 | public void MethodB(string arg) 8 | { 9 | MethodA(arg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testSlnx/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/CSharpLanguageServer.Tests/TestData/testSlnx/Test.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------