├── .config └── dotnet-tools.json ├── .editorconfig ├── .fantomasignore ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.targets ├── LICENSE.md ├── LanguageServerProtocol.sln ├── README.md ├── data └── 3.17.0 │ ├── metaModel.json │ └── metaModel.schema.json ├── global.json ├── src ├── Client.fs ├── ClientServer.cg.fs ├── FsLibLog.fs ├── Ionide.LanguageServerProtocol.fsproj ├── JsonRpc.fs ├── JsonUtils.fs ├── LanguageServerProtocol.fs ├── OptionConverter.fs ├── Server.fs ├── TypeDefaults.fs ├── Types.cg.fs └── Types.fs ├── tests ├── Benchmarks.fs ├── Ionide.LanguageServerProtocol.Tests.fsproj ├── Program.fs ├── Shotgun.fs ├── StartWithSetup.fs ├── Tests.fs └── Utils.fs └── tools └── MetaModelGenerator ├── Common.fs ├── GenerateClientServer.fs ├── GenerateTypes.fs ├── MetaModel.fs ├── MetaModelGenerator.fsproj ├── PrimitiveExtensions.fs └── Program.fs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fantomas": { 6 | "version": "7.0.1", 7 | "commands": [ 8 | "fantomas" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false 13 | max_line_length=120 14 | fsharp_max_if_then_else_short_width=60 15 | fsharp_max_record_width=80 16 | fsharp_max_array_or_list_width=80 17 | fsharp_max_value_binding_width=120 18 | fsharp_max_function_binding_width=120 19 | fsharp_max_dot_get_expression_width=120 20 | fsharp_multiline_bracket_style = stroustrup 21 | fsharp_keep_max_number_of_blank_lines=2 22 | fsharp_max_array_or_list_number_of_items=1 23 | fsharp_array_or_list_multiline_formatter=number_of_items 24 | fsharp_max_infix_operator_expression=10 25 | fsharp_multi_line_lambda_closing_newline=true -------------------------------------------------------------------------------- /.fantomasignore: -------------------------------------------------------------------------------- 1 | # Ignore all files code generated files 2 | *.cg.fs 3 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This file contains a list of git hashes of revisions to be ignored by git 2 | # These revisions are considered "unimportant" in 3 | # that they are unlikely to be what you are interested in when blaming. 4 | # Like formatting with Fantomas 5 | # https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view 6 | # Add formatting commits here 7 | 8967b8d3ed526facc42d9089d51227e7efc21267 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Custom for Visual Studio 2 | *.cs diff=csharp 3 | *.sln merge=union 4 | *.csproj merge=union 5 | *.vbproj merge=union 6 | *.fsproj merge=union 7 | *.dbproj merge=union 8 | 9 | # Standard to msysgit 10 | *.doc diff=astextplain 11 | *.DOC diff=astextplain 12 | *.docx diff=astextplain 13 | *.DOCX diff=astextplain 14 | *.dot diff=astextplain 15 | *.DOT diff=astextplain 16 | *.pdf diff=astextplain 17 | *.PDF diff=astextplain 18 | *.rtf diff=astextplain 19 | *.RTF diff=astextplain 20 | 21 | *.sh text eol=lf 22 | Makefile.orig text eol=lf 23 | configure.sh text eol=lf 24 | 25 | *.fs text eol=lf 26 | *.fsi text eol=lf 27 | *.fsx text eol=lf -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### WHAT 2 | copilot:summary 3 | 4 | copilot:poem 5 | 6 | copilot:emoji 7 | 8 | ### WHY 9 | 10 | 11 | ### HOW 12 | copilot:walkthrough 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macOS-latest] 10 | fail-fast: false # we have timing issues on some OS, so we want them all to run 11 | runs-on: ${{ matrix.os }} 12 | timeout-minutes: 15 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | show-progress: false 18 | - name: Setup .NET Core 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | global-json-file: global.json 22 | dotnet-version: | 23 | 8.x 24 | 9.x 25 | - name: Run build 26 | run: dotnet build -c Release src 27 | - name: Run tests 28 | run: dotnet test --logger GitHubActions 29 | - name: Run publish 30 | run: dotnet pack -o release src 31 | - name: Upload NuGet packages 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: packages-${{ matrix.os }} 35 | path: release/ 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Publish on any new tag starting with v 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - name: Set tag version 17 | id: get_version 18 | run: | 19 | VERSION=${GITHUB_REF_NAME#v} #This removes the 'v' from the tag 20 | echo Version: $VERSION 21 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 22 | 23 | - uses: actions/checkout@v3 24 | 25 | - name: Setup .NET Core 26 | uses: actions/setup-dotnet@v2 27 | with: 28 | global-json-file: global.json 29 | dotnet-version: | 30 | 8.x 31 | 9.x 32 | 33 | - name: Restore tools 34 | run: dotnet tool restore 35 | 36 | - name: Pack the library 37 | run: dotnet pack -c Release -o release src 38 | 39 | - name: Get Changelog Entry 40 | id: changelog_reader 41 | uses: mindsers/changelog-reader-action@v2 42 | with: 43 | version: ${{ steps.get_version.outputs.VERSION }} 44 | path: ./CHANGELOG.md 45 | validation_level: warn 46 | 47 | - name: Push the package to NuGet 48 | run: dotnet nuget push release/Ionide.LanguageServerProtocol.${{ steps.get_version.outputs.VERSION }}.nupkg --source "$env:NUGET_SOURCE" --api-key "$env:NUGET_KEY" 49 | env: 50 | NUGET_KEY: ${{ secrets.NUGET_KEY }} 51 | NUGET_SOURCE: "https://api.nuget.org/v3/index.json" 52 | 53 | - name: Create Release 54 | uses: actions/create-release@latest 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | tag_name: ${{ github.ref }} 59 | release_name: ${{ github.ref }} 60 | body: ${{ steps.changelog_reader.outputs.changes }} 61 | draft: false 62 | prerelease: false 63 | 64 | - name: Upload binaries to release 65 | uses: svenstaro/upload-release-action@v2 66 | with: 67 | repo_token: ${{ secrets.GITHUB_TOKEN }} 68 | file: release/*.nupkg 69 | tag: ${{ github.ref }} 70 | overwrite: true 71 | file_glob: true 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | release 4 | BenchmarkDotNet.Artifacts 5 | .idea 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // "editor.formatOnSave": true, 3 | "yaml.schemas": { 4 | "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json": ".github/workflows/**" 5 | }, 6 | "cSpell.words": [ 7 | "Ionide", 8 | "Newtonsoft", 9 | "Supertypes" 10 | ], 11 | "editor.formatOnSave": true 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.7.0] - 11.03.2025 8 | 9 | ### Changed 10 | 11 | * BREAKING: [Generate Client and Server interfaces from LSP spec](https://github.com/ionide/LanguageServerProtocol/pull/67) (Thanks @TheAngryByrd). The Client and Server interfaces will reflect 3.17 the LSP spec. 12 | 13 | ## [0.6.0] - 12.06.2024 14 | 15 | ### Changed 16 | 17 | * [Generate the LSP request and response types from the LSP spec.](https://github.com/ionide/LanguageServerProtocol/pull/49) (@TheAngryByrd is the GOAT for this). We now expose the full surface area of the 3.17 LSP spec. 18 | 19 | ## [0.5.1] - 09.05.2024 20 | 21 | ### Added 22 | [Types: import more RegistrationOptions types from LSP spec](https://github.com/ionide/LanguageServerProtocol/pull/64) (Thanks @razzmatazz) 23 | 24 | ### Removed 25 | [Types: Do not use Obsolete attribute to annotate deprecated types in LSP](https://github.com/ionide/LanguageServerProtocol/pull/63) (Thanks @razzmatazz) 26 | 27 | ## [0.5.0] - 26.04.2024 28 | 29 | * [Support JsonRpc via web sockets](https://github.com/ionide/LanguageServerProtocol/pull/62) (Thanks @dawedawe) 30 | 31 | ## [0.4.23] - 31.01.2024 32 | 33 | ### Added 34 | * [Add TriggerForIncompleteCompletions & Capitalize the first char of fields of CompletionContext](https://github.com/ionide/LanguageServerProtocol/pull/61) (thanks @tcx4c70) 35 | 36 | ## [0.4.22] - 15.01.2024 37 | 38 | ### Added 39 | * [New Error Code members to match spec-provided error codes in 3.17.0](https://github.com/ionide/LanguageServerProtocol/pull/59) 40 | 41 | ### Changed 42 | * [Added obsoletion messages to members marked deprecated in the spec](https://github.com/ionide/LanguageServerProtocol/pull/59) 43 | 44 | ## [0.4.21] - 29.10.2023 45 | 46 | ### Fixed 47 | * [Fix WorkDoneProgressCancel arguments](https://github.com/ionide/LanguageServerProtocol/pull/58) (thanks @TheAngryByrd) 48 | 49 | ## [0.4.20] - 28.10.2023 50 | 51 | ### Changed 52 | * [Update StreamJsonRpc to 2.16.36](https://github.com/ionide/LanguageServerProtocol/pull/57) (thanks @TheAngryByrd) 53 | 54 | ## [0.4.19] - 28.10.2023 55 | 56 | ### Fixed 57 | * [Add default for WorkspaceSymbolResolve](https://github.com/ionide/LanguageServerProtocol/pull/56) (thanks @nojaf) 58 | * [Fix crashing on json serialization](https://github.com/ionide/LanguageServerProtocol/pull/55) (thanks @TheAngryByrd) 59 | 60 | ## [0.4.18] - 22.10.2023 61 | 62 | ### Changed 63 | 64 | * **BREAKING Change** [Fixing Spelling](https://github.com/ionide/LanguageServerProtocol/pull/54) (Thanks @TheAngryByrd) 65 | 66 | ## [0.4.17] - 27.07.2023 67 | 68 | ### Added 69 | 70 | * Update a type signature on the SignatureInformation structure 71 | 72 | ## [0.4.16] - 27.07.2023 73 | 74 | ### Added 75 | 76 | * [Add missing features for LSP Specification 3.17](https://github.com/ionide/LanguageServerProtocol/pull/52) (thanks @tcx4c70!) 77 | 78 | ## [0.4.15] - 13.06.2023 79 | 80 | ### Fixed 81 | 82 | * [Allow multiple instances of the server on a single process](https://github.com/ionide/LanguageServerProtocol/pull/51) (thanks @TrevorThoele2!) 83 | 84 | ## [0.4.14] - 04.04.2023 85 | 86 | ### Fixed 87 | 88 | * [PublishDiagnosticsCapabilities should be optional](https://github.com/ionide/LanguageServerProtocol/pull/48) (thanks @sharpSteff!) 89 | 90 | ## [0.4.13] - 05.03.2023 91 | 92 | ### Added 93 | 94 | * [Added types and bindings for the following endpoints](https://github.com/ionide/LanguageServerProtocol/pull/46) (thanks @tcs4c70!) 95 | * textDocument/prepareCallHierarchy 96 | * callHierarchy/incomingCalls 97 | * callHierarchy/outgoingCalls 98 | * textDocument/prepareTypeHierarchy 99 | * typeHierarchy/supertypes 100 | * typeHierarchy/subtypes 101 | 102 | ## [0.4.12] - 21.12.2022 103 | 104 | ### Added 105 | 106 | * [Add textDocument/inlineValue types and functionality](https://github.com/ionide/LanguageServerProtocol/pull/44) (thanks @kaashyapan!) 107 | 108 | ## [0.4.11] - 12.12.2022 109 | 110 | ### Added 111 | 112 | * [Add types for Progress and WorkDone](https://github.com/ionide/LanguageServerProtocol/pull/43) (thanks @TheAngryByrd!) 113 | 114 | ## [0.4.10] - 30.09.2022 115 | 116 | ### Added 117 | 118 | * Fix type constraints for diagnostics sources to prevent serialization errors 119 | 120 | ## [0.4.9] - 21.09.2022 121 | 122 | ### Added 123 | 124 | * [Allow consumers to customize the JsonRpc used](https://github.com/ionide/LanguageServerProtocol/pull/40) 125 | 126 | ## [0.4.8] - 12.09.2022 127 | 128 | ### Added 129 | 130 | * [Introduce interfaces for LSP server and client to allow for easier testing](https://github.com/ionide/LanguageServerProtocol/pull/37) (thanks @TheAngryByrd!) 131 | 132 | ## [0.4.7] - 24.08.2022 133 | 134 | ### Added 135 | 136 | * [Client workspace capabilities for Code Lenses](https://github.com/ionide/LanguageServerProtocol/pull/34) 137 | 138 | ## [0.4.6] - 23.08.2022 139 | 140 | ### Added 141 | 142 | * [Error.RequestCancelled support](https://github.com/ionide/LanguageServerProtocol/pull/31) (thanks @razzmatazz!) 143 | 144 | ### Fixed 145 | 146 | * [Make server-reported LSP errors not crash the transport](https://github.com/ionide/LanguageServerProtocol/pull/33) (thanks @razzmatazz!) 147 | 148 | ## [0.4.5] - 07.08.2022 149 | 150 | ### Added 151 | 152 | * [textDocument/prepareRename types and functionality](https://github.com/ionide/LanguageServerProtocol/pull/30) and [client/server capabilities](https://github.com/ionide/LanguageServerProtocol/pull/31) (thanks @artempyanykh!) 153 | 154 | ### Changed 155 | 156 | * [JsonRpc no longer swallows exceptions](https://github.com/ionide/LanguageServerProtocol/pull/29) (thanks @artempyanykh!) 157 | 158 | ## [0.4.4] - 27.06.2022 159 | 160 | ### Added 161 | 162 | * [Deserialization support for erased unions](https://github.com/ionide/LanguageServerProtocol/pull/27) (Thanks @Booksbaum!) 163 | 164 | ## [0.4.3] - 08.06.2022 165 | 166 | ### Fixed 167 | 168 | * [Fix a typo in the workspace/executeCommand registration](https://github.com/ionide/LanguageServerProtocol/pull/28) (Thanks @keynmol!) 169 | 170 | ## [0.4.2] - 26.05.2022 171 | 172 | ### Fixed 173 | 174 | * [Make the inlayHint client capability optional](https://github.com/ionide/LanguageServerProtocol/pull/23) (thanks @artempyanykh!) 175 | * [Handle exceptions from serialization](https://github.com/ionide/LanguageServerProtocol/pull/25) (thanks @artempyanykh!) 176 | * [Make the InlayHintWorkspaceClientCapabilities part of WorkspaceClientCapabilities](https://github.com/ionide/LanguageServerProtocol/pull/26) (thanks @Booksbaum!) 177 | 178 | ## [0.4.1] - 14.05.2022 179 | 180 | ### Changed 181 | 182 | * [`textDocument/symbol` now returns `DocumentSymbol[]` instead of `SymbolInformation[]`](https://github.com/ionide/LanguageServerProtocol/pull/18) (thanks @artempyanykh!) 183 | 184 | ### Fixed 185 | 186 | * [Workaround a VSCode language client bug preventing server shutdown](https://github.com/ionide/LanguageServerProtocol/pull/21) (thanks @artempyanykh!) 187 | 188 | ### Added 189 | 190 | * [Types and methods for InlayHint support](https://github.com/ionide/LanguageServerProtocol/pull/22) (thanks @Booksbaum!) 191 | 192 | ## [0.4.0] - 28.04.2022 193 | 194 | ### Added 195 | 196 | * [Add types for workspace folders](https://github.com/ionide/LanguageServerProtocol/pull/15) (thanks @artempyanykh!) 197 | * [Add types for workspace file notifications](https://github.com/ionide/LanguageServerProtocol/pull/17) (thanks @artempyanykh!) 198 | 199 | ### Changed 200 | 201 | * [Use the StreamJsonRpc library as the transport layer instead of our own](https://github.com/ionide/LanguageServerProtocol/pull/10) (thanks @razzmatazz!) 202 | 203 | 204 | ## [0.3.1] - 8.1.2022 205 | 206 | ### Added 207 | 208 | * Add XmlDocs to the generated package 209 | 210 | ## [0.3.0] - 23.11.2021 211 | 212 | ### Added 213 | 214 | * Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz) 215 | * Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz) 216 | 217 | ## [0.2.0] - 17.11.2021 218 | 219 | ### Added 220 | 221 | * Add support for `codeAction/resolve` (by @razzmatazz) 222 | 223 | ## [0.1.1] - 15.11.2021 224 | 225 | ### Added 226 | 227 | * Initial implementation 228 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lambda_factory@outlook.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved! 4 | 5 | ## Using the issue tracker 6 | 7 | Use the issues tracker for: 8 | 9 | - [bug reports](#bug-reports) 10 | - [feature requests](#feature-requests) 11 | - [submitting pull requests](#pull-requests) 12 | - [releasing](#release) 13 | 14 | ## Bug reports 15 | 16 | A bug is either a _demonstrable problem_ that is caused in extension failing to provide the expected feature or indicate missing, unclear, or misleading documentation. Good bug reports are extremely helpful - thank you! 17 | 18 | Guidelines for bug reports: 19 | 20 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 21 | 22 | 2. **Check if the issue has been fixed** — try to reproduce it using the `main` branch in the repository. 23 | 24 | 3. **Isolate and report the problem** — ideally create a reduced test case. 25 | 26 | Please try to be as detailed as possible in your report. Include information about 27 | version of the VSCode and extension. Please provide steps to 28 | reproduce the issue as well as the outcome you were expecting! All these details 29 | will help developers to fix any potential bugs. 30 | 31 | ## Feature requests 32 | 33 | Feature requests are welcome and should be discussed on issue tracker. But take a moment to find 34 | out whether your idea fits with the scope and aims of the project. It's up to _you_ 35 | to make a strong case to convince the community of the merits of this feature. 36 | Please provide as much detail and context as possible. 37 | 38 | ## Pull requests 39 | 40 | Good pull requests - patches, improvements, new features - are a fantastic 41 | help. They should remain focused in scope and avoid containing unrelated 42 | commits. 43 | 44 | **IMPORTANT**: By submitting a patch, you agree that your work will be 45 | licensed under the license used by the project. 46 | 47 | If you have any large pull request in mind (e.g. implementing features, 48 | refactoring code, etc), **please ask first** otherwise you risk spending 49 | a lot of time working on something that the project's developers might 50 | not want to merge into the project. 51 | 52 | Please adhere to the coding conventions in the project (indentation, 53 | accurate comments, etc.). 54 | 55 | 56 | ## Release 57 | 58 | 1. Update version in CHANGELOG.md and add notes 59 | 1. If possible link the pull request of the changes and mention the author of the pull request 60 | 2. Create new commit 61 | 1. `git add CHANGELOG.md` 62 | 1. `git commit -m "changelog for v0.45.0"` 63 | 3. Make a new version tag (for example, `v0.45.0`) 64 | 1. `git tag v0.45.0` 65 | 4. Push changes to the repo. 66 | 1. `git push --atomic origin main v0.45.0` 67 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | <_BuildProjBaseIntermediateOutputPath>$(MSBuildThisFileDirectory)build/obj/ 10 | <_DotnetToolManifestFile>$(MSBuildThisFileDirectory).config/dotnet-tools.json 11 | <_DotnetToolRestoreOutputFile> 12 | $(_BuildProjBaseIntermediateOutputPath)/dotnet-tool-restore-$(NETCoreSdkVersion)-$(OS) 13 | <_DotnetFantomasOutputFile> 14 | $(BaseIntermediateOutputPath)dotnet-fantomas-msbuild-$(NETCoreSdkVersion)-$(OS) 15 | 16 | 17 | 18 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Krzysztof Cieslak & Ionide Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /LanguageServerProtocol.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("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Ionide.LanguageServerProtocol", "src\Ionide.LanguageServerProtocol.fsproj", "{CA3DF91E-B82C-4DFC-BDBC-CE383717E457}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Ionide.LanguageServerProtocol.Tests", "tests\Ionide.LanguageServerProtocol.Tests.fsproj", "{8E54FA2A-C7E4-4D70-AF23-7F8D56EB6B9C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{47733741-07EF-431B-A1DC-2A22E4090CCC}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MetaModelGenerator", "tools\MetaModelGenerator\MetaModelGenerator.fsproj", "{4BA36C85-8414-4965-AAA3-B6D8EDE14B7D}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {CA3DF91E-B82C-4DFC-BDBC-CE383717E457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {CA3DF91E-B82C-4DFC-BDBC-CE383717E457}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {CA3DF91E-B82C-4DFC-BDBC-CE383717E457}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {CA3DF91E-B82C-4DFC-BDBC-CE383717E457}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {8E54FA2A-C7E4-4D70-AF23-7F8D56EB6B9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {8E54FA2A-C7E4-4D70-AF23-7F8D56EB6B9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {8E54FA2A-C7E4-4D70-AF23-7F8D56EB6B9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {8E54FA2A-C7E4-4D70-AF23-7F8D56EB6B9C}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {4BA36C85-8414-4965-AAA3-B6D8EDE14B7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {4BA36C85-8414-4965-AAA3-B6D8EDE14B7D}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {4BA36C85-8414-4965-AAA3-B6D8EDE14B7D}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {4BA36C85-8414-4965-AAA3-B6D8EDE14B7D}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(NestedProjects) = preSolution 37 | {4BA36C85-8414-4965-AAA3-B6D8EDE14B7D} = {47733741-07EF-431B-A1DC-2A22E4090CCC} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Language Server Protocol 2 | 3 | Library for implementing Language Server Protocol in F#. 4 | 5 | ## How to build 6 | 7 | 1. dotnet build 8 | 9 | ## How to contribute 10 | 11 | _Imposter syndrome disclaimer_: I want your help. No really, I do. 12 | 13 | There might be a little voice inside that tells you you're not ready; that you need to do one more tutorial, or learn another framework, or write a few more blog posts before you can help me with this project. 14 | 15 | I assure you, that's not the case. 16 | 17 | This project has some clear Contribution Guidelines and expectations that you can [read here](CONTRIBUTING.md). 18 | 19 | The contribution guidelines outline the process that you'll need to follow to get a patch merged. By making expectations and process explicit, I hope it will make it easier for you to contribute. 20 | 21 | And you don't just have to write code. You can help out by writing documentation, tests, or even by giving feedback about this work. (And yes, that includes giving feedback about the contribution guidelines.) 22 | 23 | Thank you for contributing! 24 | 25 | ## Contributing and copyright 26 | 27 | The project is hosted on [GitHub](https://github.com/ionide/LanguageServerProtocol) where you can [report issues](https://github.com/ionide/LanguageServerProtocol/issues), participate in [discussions](https://github.com/ionide/LanguageServerProtocol/discussions), fork 28 | the project and submit pull requests. 29 | 30 | The library is available under [MIT license](LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes. 31 | 32 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 33 | 34 | ## Our Sponsors 35 | 36 | Discover Panel is released as part of Ionide organization - OSS organization focused on building cross platform, developer tools. 37 | 38 | You can support Ionide development on [Open Collective](https://opencollective.com/ionide). 39 | 40 | ### Partners 41 | 42 |
43 | 44 | drawing 45 | 46 |
47 | 48 | ### Sponsors 49 | 50 | [Become a sponsor](https://opencollective.com/ionide) and get your logo on our README on Github, description in the VSCode marketplace and on [ionide.io](https://ionide.io) with a link to your site. 51 | 52 |
53 | 54 | 55 |
56 | 57 |
58 |
59 | -------------------------------------------------------------------------------- /data/3.17.0/metaModel.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "AndType": { 5 | "additionalProperties": false, 6 | "description": "Represents an `and`type (e.g. TextDocumentParams & WorkDoneProgressParams`).", 7 | "properties": { 8 | "items": { 9 | "items": { 10 | "$ref": "#/definitions/Type" 11 | }, 12 | "type": "array" 13 | }, 14 | "kind": { 15 | "const": "and", 16 | "type": "string" 17 | } 18 | }, 19 | "required": [ 20 | "kind", 21 | "items" 22 | ], 23 | "type": "object" 24 | }, 25 | "ArrayType": { 26 | "additionalProperties": false, 27 | "description": "Represents an array type (e.g. `TextDocument[]`).", 28 | "properties": { 29 | "element": { 30 | "$ref": "#/definitions/Type" 31 | }, 32 | "kind": { 33 | "const": "array", 34 | "type": "string" 35 | } 36 | }, 37 | "required": [ 38 | "kind", 39 | "element" 40 | ], 41 | "type": "object" 42 | }, 43 | "BaseType": { 44 | "additionalProperties": false, 45 | "description": "Represents a base type like `string` or `DocumentUri`.", 46 | "properties": { 47 | "kind": { 48 | "const": "base", 49 | "type": "string" 50 | }, 51 | "name": { 52 | "$ref": "#/definitions/BaseTypes" 53 | } 54 | }, 55 | "required": [ 56 | "kind", 57 | "name" 58 | ], 59 | "type": "object" 60 | }, 61 | "BaseTypes": { 62 | "enum": [ 63 | "URI", 64 | "DocumentUri", 65 | "integer", 66 | "uinteger", 67 | "decimal", 68 | "RegExp", 69 | "string", 70 | "boolean", 71 | "null" 72 | ], 73 | "type": "string" 74 | }, 75 | "BooleanLiteralType": { 76 | "additionalProperties": false, 77 | "description": "Represents a boolean literal type (e.g. `kind: true`).", 78 | "properties": { 79 | "kind": { 80 | "const": "booleanLiteral", 81 | "type": "string" 82 | }, 83 | "value": { 84 | "type": "boolean" 85 | } 86 | }, 87 | "required": [ 88 | "kind", 89 | "value" 90 | ], 91 | "type": "object" 92 | }, 93 | "Enumeration": { 94 | "additionalProperties": false, 95 | "description": "Defines an enumeration.", 96 | "properties": { 97 | "deprecated": { 98 | "description": "Whether the enumeration is deprecated or not. If deprecated the property contains the deprecation message.", 99 | "type": "string" 100 | }, 101 | "documentation": { 102 | "description": "An optional documentation.", 103 | "type": "string" 104 | }, 105 | "name": { 106 | "description": "The name of the enumeration.", 107 | "type": "string" 108 | }, 109 | "proposed": { 110 | "description": "Whether this is a proposed enumeration. If omitted, the enumeration is final.", 111 | "type": "boolean" 112 | }, 113 | "since": { 114 | "description": "Since when (release number) this enumeration is available. Is undefined if not known.", 115 | "type": "string" 116 | }, 117 | "supportsCustomValues": { 118 | "description": "Whether the enumeration supports custom values (e.g. values which are not part of the set defined in `values`). If omitted no custom values are supported.", 119 | "type": "boolean" 120 | }, 121 | "type": { 122 | "$ref": "#/definitions/EnumerationType", 123 | "description": "The type of the elements." 124 | }, 125 | "values": { 126 | "description": "The enum values.", 127 | "items": { 128 | "$ref": "#/definitions/EnumerationEntry" 129 | }, 130 | "type": "array" 131 | } 132 | }, 133 | "required": [ 134 | "name", 135 | "type", 136 | "values" 137 | ], 138 | "type": "object" 139 | }, 140 | "EnumerationEntry": { 141 | "additionalProperties": false, 142 | "description": "Defines an enumeration entry.", 143 | "properties": { 144 | "deprecated": { 145 | "description": "Whether the enum entry is deprecated or not. If deprecated the property contains the deprecation message.", 146 | "type": "string" 147 | }, 148 | "documentation": { 149 | "description": "An optional documentation.", 150 | "type": "string" 151 | }, 152 | "name": { 153 | "description": "The name of the enum item.", 154 | "type": "string" 155 | }, 156 | "proposed": { 157 | "description": "Whether this is a proposed enumeration entry. If omitted, the enumeration entry is final.", 158 | "type": "boolean" 159 | }, 160 | "since": { 161 | "description": "Since when (release number) this enumeration entry is available. Is undefined if not known.", 162 | "type": "string" 163 | }, 164 | "value": { 165 | "description": "The value.", 166 | "type": [ 167 | "string", 168 | "number" 169 | ] 170 | } 171 | }, 172 | "required": [ 173 | "name", 174 | "value" 175 | ], 176 | "type": "object" 177 | }, 178 | "EnumerationType": { 179 | "additionalProperties": false, 180 | "properties": { 181 | "kind": { 182 | "const": "base", 183 | "type": "string" 184 | }, 185 | "name": { 186 | "enum": [ 187 | "string", 188 | "integer", 189 | "uinteger" 190 | ], 191 | "type": "string" 192 | } 193 | }, 194 | "required": [ 195 | "kind", 196 | "name" 197 | ], 198 | "type": "object" 199 | }, 200 | "IntegerLiteralType": { 201 | "additionalProperties": false, 202 | "properties": { 203 | "kind": { 204 | "const": "integerLiteral", 205 | "description": "Represents an integer literal type (e.g. `kind: 1`).", 206 | "type": "string" 207 | }, 208 | "value": { 209 | "type": "number" 210 | } 211 | }, 212 | "required": [ 213 | "kind", 214 | "value" 215 | ], 216 | "type": "object" 217 | }, 218 | "MapKeyType": { 219 | "anyOf": [ 220 | { 221 | "additionalProperties": false, 222 | "properties": { 223 | "kind": { 224 | "const": "base", 225 | "type": "string" 226 | }, 227 | "name": { 228 | "enum": [ 229 | "URI", 230 | "DocumentUri", 231 | "string", 232 | "integer" 233 | ], 234 | "type": "string" 235 | } 236 | }, 237 | "required": [ 238 | "kind", 239 | "name" 240 | ], 241 | "type": "object" 242 | }, 243 | { 244 | "$ref": "#/definitions/ReferenceType" 245 | } 246 | ], 247 | "description": "Represents a type that can be used as a key in a map type. If a reference type is used then the type must either resolve to a `string` or `integer` type. (e.g. `type ChangeAnnotationIdentifier === string`)." 248 | }, 249 | "MapType": { 250 | "additionalProperties": false, 251 | "description": "Represents a JSON object map (e.g. `interface Map { [key: K] => V; }`).", 252 | "properties": { 253 | "key": { 254 | "$ref": "#/definitions/MapKeyType" 255 | }, 256 | "kind": { 257 | "const": "map", 258 | "type": "string" 259 | }, 260 | "value": { 261 | "$ref": "#/definitions/Type" 262 | } 263 | }, 264 | "required": [ 265 | "kind", 266 | "key", 267 | "value" 268 | ], 269 | "type": "object" 270 | }, 271 | "MessageDirection": { 272 | "description": "Indicates in which direction a message is sent in the protocol.", 273 | "enum": [ 274 | "clientToServer", 275 | "serverToClient", 276 | "both" 277 | ], 278 | "type": "string" 279 | }, 280 | "MetaData": { 281 | "additionalProperties": false, 282 | "properties": { 283 | "version": { 284 | "description": "The protocol version.", 285 | "type": "string" 286 | } 287 | }, 288 | "required": [ 289 | "version" 290 | ], 291 | "type": "object" 292 | }, 293 | "MetaModel": { 294 | "additionalProperties": false, 295 | "description": "The actual meta model.", 296 | "properties": { 297 | "enumerations": { 298 | "description": "The enumerations.", 299 | "items": { 300 | "$ref": "#/definitions/Enumeration" 301 | }, 302 | "type": "array" 303 | }, 304 | "metaData": { 305 | "$ref": "#/definitions/MetaData", 306 | "description": "Additional meta data." 307 | }, 308 | "notifications": { 309 | "description": "The notifications.", 310 | "items": { 311 | "$ref": "#/definitions/Notification" 312 | }, 313 | "type": "array" 314 | }, 315 | "requests": { 316 | "description": "The requests.", 317 | "items": { 318 | "$ref": "#/definitions/Request" 319 | }, 320 | "type": "array" 321 | }, 322 | "structures": { 323 | "description": "The structures.", 324 | "items": { 325 | "$ref": "#/definitions/Structure" 326 | }, 327 | "type": "array" 328 | }, 329 | "typeAliases": { 330 | "description": "The type aliases.", 331 | "items": { 332 | "$ref": "#/definitions/TypeAlias" 333 | }, 334 | "type": "array" 335 | } 336 | }, 337 | "required": [ 338 | "metaData", 339 | "requests", 340 | "notifications", 341 | "structures", 342 | "enumerations", 343 | "typeAliases" 344 | ], 345 | "type": "object" 346 | }, 347 | "Notification": { 348 | "additionalProperties": false, 349 | "description": "Represents a LSP notification", 350 | "properties": { 351 | "deprecated": { 352 | "description": "Whether the notification is deprecated or not. If deprecated the property contains the deprecation message.", 353 | "type": "string" 354 | }, 355 | "documentation": { 356 | "description": "An optional documentation;", 357 | "type": "string" 358 | }, 359 | "messageDirection": { 360 | "$ref": "#/definitions/MessageDirection", 361 | "description": "The direction in which this notification is sent in the protocol." 362 | }, 363 | "method": { 364 | "description": "The request's method name.", 365 | "type": "string" 366 | }, 367 | "params": { 368 | "anyOf": [ 369 | { 370 | "$ref": "#/definitions/Type" 371 | }, 372 | { 373 | "items": { 374 | "$ref": "#/definitions/Type" 375 | }, 376 | "type": "array" 377 | } 378 | ], 379 | "description": "The parameter type(s) if any." 380 | }, 381 | "proposed": { 382 | "description": "Whether this is a proposed notification. If omitted the notification is final.", 383 | "type": "boolean" 384 | }, 385 | "registrationMethod": { 386 | "description": "Optional a dynamic registration method if it different from the request's method.", 387 | "type": "string" 388 | }, 389 | "registrationOptions": { 390 | "$ref": "#/definitions/Type", 391 | "description": "Optional registration options if the notification supports dynamic registration." 392 | }, 393 | "since": { 394 | "description": "Since when (release number) this notification is available. Is undefined if not known.", 395 | "type": "string" 396 | } 397 | }, 398 | "required": [ 399 | "method", 400 | "messageDirection" 401 | ], 402 | "type": "object" 403 | }, 404 | "OrType": { 405 | "additionalProperties": false, 406 | "description": "Represents an `or` type (e.g. `Location | LocationLink`).", 407 | "properties": { 408 | "items": { 409 | "items": { 410 | "$ref": "#/definitions/Type" 411 | }, 412 | "type": "array" 413 | }, 414 | "kind": { 415 | "const": "or", 416 | "type": "string" 417 | } 418 | }, 419 | "required": [ 420 | "kind", 421 | "items" 422 | ], 423 | "type": "object" 424 | }, 425 | "Property": { 426 | "additionalProperties": false, 427 | "description": "Represents an object property.", 428 | "properties": { 429 | "deprecated": { 430 | "description": "Whether the property is deprecated or not. If deprecated the property contains the deprecation message.", 431 | "type": "string" 432 | }, 433 | "documentation": { 434 | "description": "An optional documentation.", 435 | "type": "string" 436 | }, 437 | "name": { 438 | "description": "The property name;", 439 | "type": "string" 440 | }, 441 | "optional": { 442 | "description": "Whether the property is optional. If omitted, the property is mandatory.", 443 | "type": "boolean" 444 | }, 445 | "proposed": { 446 | "description": "Whether this is a proposed property. If omitted, the structure is final.", 447 | "type": "boolean" 448 | }, 449 | "since": { 450 | "description": "Since when (release number) this property is available. Is undefined if not known.", 451 | "type": "string" 452 | }, 453 | "type": { 454 | "$ref": "#/definitions/Type", 455 | "description": "The type of the property" 456 | } 457 | }, 458 | "required": [ 459 | "name", 460 | "type" 461 | ], 462 | "type": "object" 463 | }, 464 | "ReferenceType": { 465 | "additionalProperties": false, 466 | "description": "Represents a reference to another type (e.g. `TextDocument`). This is either a `Structure`, a `Enumeration` or a `TypeAlias` in the same meta model.", 467 | "properties": { 468 | "kind": { 469 | "const": "reference", 470 | "type": "string" 471 | }, 472 | "name": { 473 | "type": "string" 474 | } 475 | }, 476 | "required": [ 477 | "kind", 478 | "name" 479 | ], 480 | "type": "object" 481 | }, 482 | "Request": { 483 | "additionalProperties": false, 484 | "description": "Represents a LSP request", 485 | "properties": { 486 | "deprecated": { 487 | "description": "Whether the request is deprecated or not. If deprecated the property contains the deprecation message.", 488 | "type": "string" 489 | }, 490 | "documentation": { 491 | "description": "An optional documentation;", 492 | "type": "string" 493 | }, 494 | "errorData": { 495 | "$ref": "#/definitions/Type", 496 | "description": "An optional error data type." 497 | }, 498 | "messageDirection": { 499 | "$ref": "#/definitions/MessageDirection", 500 | "description": "The direction in which this request is sent in the protocol." 501 | }, 502 | "method": { 503 | "description": "The request's method name.", 504 | "type": "string" 505 | }, 506 | "params": { 507 | "anyOf": [ 508 | { 509 | "$ref": "#/definitions/Type" 510 | }, 511 | { 512 | "items": { 513 | "$ref": "#/definitions/Type" 514 | }, 515 | "type": "array" 516 | } 517 | ], 518 | "description": "The parameter type(s) if any." 519 | }, 520 | "partialResult": { 521 | "$ref": "#/definitions/Type", 522 | "description": "Optional partial result type if the request supports partial result reporting." 523 | }, 524 | "proposed": { 525 | "description": "Whether this is a proposed feature. If omitted the feature is final.", 526 | "type": "boolean" 527 | }, 528 | "registrationMethod": { 529 | "description": "Optional a dynamic registration method if it different from the request's method.", 530 | "type": "string" 531 | }, 532 | "registrationOptions": { 533 | "$ref": "#/definitions/Type", 534 | "description": "Optional registration options if the request supports dynamic registration." 535 | }, 536 | "result": { 537 | "$ref": "#/definitions/Type", 538 | "description": "The result type." 539 | }, 540 | "since": { 541 | "description": "Since when (release number) this request is available. Is undefined if not known.", 542 | "type": "string" 543 | } 544 | }, 545 | "required": [ 546 | "method", 547 | "result", 548 | "messageDirection" 549 | ], 550 | "type": "object" 551 | }, 552 | "StringLiteralType": { 553 | "additionalProperties": false, 554 | "description": "Represents a string literal type (e.g. `kind: 'rename'`).", 555 | "properties": { 556 | "kind": { 557 | "const": "stringLiteral", 558 | "type": "string" 559 | }, 560 | "value": { 561 | "type": "string" 562 | } 563 | }, 564 | "required": [ 565 | "kind", 566 | "value" 567 | ], 568 | "type": "object" 569 | }, 570 | "Structure": { 571 | "additionalProperties": false, 572 | "description": "Defines the structure of an object literal.", 573 | "properties": { 574 | "deprecated": { 575 | "description": "Whether the structure is deprecated or not. If deprecated the property contains the deprecation message.", 576 | "type": "string" 577 | }, 578 | "documentation": { 579 | "description": "An optional documentation;", 580 | "type": "string" 581 | }, 582 | "extends": { 583 | "description": "Structures extended from. This structures form a polymorphic type hierarchy.", 584 | "items": { 585 | "$ref": "#/definitions/Type" 586 | }, 587 | "type": "array" 588 | }, 589 | "mixins": { 590 | "description": "Structures to mix in. The properties of these structures are `copied` into this structure. Mixins don't form a polymorphic type hierarchy in LSP.", 591 | "items": { 592 | "$ref": "#/definitions/Type" 593 | }, 594 | "type": "array" 595 | }, 596 | "name": { 597 | "description": "The name of the structure.", 598 | "type": "string" 599 | }, 600 | "properties": { 601 | "description": "The properties.", 602 | "items": { 603 | "$ref": "#/definitions/Property" 604 | }, 605 | "type": "array" 606 | }, 607 | "proposed": { 608 | "description": "Whether this is a proposed structure. If omitted, the structure is final.", 609 | "type": "boolean" 610 | }, 611 | "since": { 612 | "description": "Since when (release number) this structure is available. Is undefined if not known.", 613 | "type": "string" 614 | } 615 | }, 616 | "required": [ 617 | "name", 618 | "properties" 619 | ], 620 | "type": "object" 621 | }, 622 | "StructureLiteral": { 623 | "additionalProperties": false, 624 | "description": "Defines an unnamed structure of an object literal.", 625 | "properties": { 626 | "deprecated": { 627 | "description": "Whether the literal is deprecated or not. If deprecated the property contains the deprecation message.", 628 | "type": "string" 629 | }, 630 | "documentation": { 631 | "description": "An optional documentation.", 632 | "type": "string" 633 | }, 634 | "properties": { 635 | "description": "The properties.", 636 | "items": { 637 | "$ref": "#/definitions/Property" 638 | }, 639 | "type": "array" 640 | }, 641 | "proposed": { 642 | "description": "Whether this is a proposed structure. If omitted, the structure is final.", 643 | "type": "boolean" 644 | }, 645 | "since": { 646 | "description": "Since when (release number) this structure is available. Is undefined if not known.", 647 | "type": "string" 648 | } 649 | }, 650 | "required": [ 651 | "properties" 652 | ], 653 | "type": "object" 654 | }, 655 | "StructureLiteralType": { 656 | "additionalProperties": false, 657 | "description": "Represents a literal structure (e.g. `property: { start: uinteger; end: uinteger; }`).", 658 | "properties": { 659 | "kind": { 660 | "const": "literal", 661 | "type": "string" 662 | }, 663 | "value": { 664 | "$ref": "#/definitions/StructureLiteral" 665 | } 666 | }, 667 | "required": [ 668 | "kind", 669 | "value" 670 | ], 671 | "type": "object" 672 | }, 673 | "TupleType": { 674 | "additionalProperties": false, 675 | "description": "Represents a `tuple` type (e.g. `[integer, integer]`).", 676 | "properties": { 677 | "items": { 678 | "items": { 679 | "$ref": "#/definitions/Type" 680 | }, 681 | "type": "array" 682 | }, 683 | "kind": { 684 | "const": "tuple", 685 | "type": "string" 686 | } 687 | }, 688 | "required": [ 689 | "kind", 690 | "items" 691 | ], 692 | "type": "object" 693 | }, 694 | "Type": { 695 | "anyOf": [ 696 | { 697 | "$ref": "#/definitions/BaseType" 698 | }, 699 | { 700 | "$ref": "#/definitions/ReferenceType" 701 | }, 702 | { 703 | "$ref": "#/definitions/ArrayType" 704 | }, 705 | { 706 | "$ref": "#/definitions/MapType" 707 | }, 708 | { 709 | "$ref": "#/definitions/AndType" 710 | }, 711 | { 712 | "$ref": "#/definitions/OrType" 713 | }, 714 | { 715 | "$ref": "#/definitions/TupleType" 716 | }, 717 | { 718 | "$ref": "#/definitions/StructureLiteralType" 719 | }, 720 | { 721 | "$ref": "#/definitions/StringLiteralType" 722 | }, 723 | { 724 | "$ref": "#/definitions/IntegerLiteralType" 725 | }, 726 | { 727 | "$ref": "#/definitions/BooleanLiteralType" 728 | } 729 | ] 730 | }, 731 | "TypeAlias": { 732 | "additionalProperties": false, 733 | "description": "Defines a type alias. (e.g. `type Definition = Location | LocationLink`)", 734 | "properties": { 735 | "deprecated": { 736 | "description": "Whether the type alias is deprecated or not. If deprecated the property contains the deprecation message.", 737 | "type": "string" 738 | }, 739 | "documentation": { 740 | "description": "An optional documentation.", 741 | "type": "string" 742 | }, 743 | "name": { 744 | "description": "The name of the type alias.", 745 | "type": "string" 746 | }, 747 | "proposed": { 748 | "description": "Whether this is a proposed type alias. If omitted, the type alias is final.", 749 | "type": "boolean" 750 | }, 751 | "since": { 752 | "description": "Since when (release number) this structure is available. Is undefined if not known.", 753 | "type": "string" 754 | }, 755 | "type": { 756 | "$ref": "#/definitions/Type", 757 | "description": "The aliased type." 758 | } 759 | }, 760 | "required": [ 761 | "name", 762 | "type" 763 | ], 764 | "type": "object" 765 | }, 766 | "TypeKind": { 767 | "enum": [ 768 | "base", 769 | "reference", 770 | "array", 771 | "map", 772 | "and", 773 | "or", 774 | "tuple", 775 | "literal", 776 | "stringLiteral", 777 | "integerLiteral", 778 | "booleanLiteral" 779 | ], 780 | "type": "string" 781 | } 782 | } 783 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100", 4 | "rollForward": "latestMinor" 5 | } 6 | } -------------------------------------------------------------------------------- /src/Client.fs: -------------------------------------------------------------------------------- 1 | namespace Ionide.LanguageServerProtocol 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonRpc 5 | 6 | module private ClientUtil = 7 | /// Return the JSON-RPC "not implemented" error 8 | let notImplemented<'t> = async.Return LspResult.notImplemented<'t> 9 | 10 | /// Do nothing and ignore the notification 11 | let ignoreNotification = async.Return(()) 12 | 13 | open ClientUtil 14 | 15 | [] 16 | type LspClient() = 17 | 18 | /// The show message notification is sent from a server to a client to ask the client to display 19 | /// a particular message in the user interface. 20 | abstract member WindowShowMessage: ShowMessageParams -> Async 21 | 22 | default __.WindowShowMessage(_) = ignoreNotification 23 | 24 | /// The show message request is sent from a server to a client to ask the client to display 25 | /// a particular message in the user interface. In addition to the show message notification the 26 | /// request allows to pass actions and to wait for an answer from the client. 27 | abstract member WindowShowMessageRequest: ShowMessageRequestParams -> AsyncLspResult 28 | 29 | default __.WindowShowMessageRequest(_) = notImplemented 30 | 31 | /// The log message notification is sent from the server to the client to ask the client to log 32 | ///a particular message. 33 | abstract member WindowLogMessage: LogMessageParams -> Async 34 | 35 | default __.WindowLogMessage(_) = ignoreNotification 36 | 37 | /// The show document request is sent from a server to a client to ask the client to display a particular 38 | /// resource referenced by a URI in the user interface. 39 | abstract member WindowShowDocument: ShowDocumentParams -> AsyncLspResult 40 | 41 | default __.WindowShowDocument(_) = notImplemented 42 | 43 | /// The telemetry notification is sent from the server to the client to ask the client to log 44 | /// a telemetry event. 45 | abstract member TelemetryEvent: Newtonsoft.Json.Linq.JToken -> Async 46 | 47 | default __.TelemetryEvent(_) = ignoreNotification 48 | 49 | /// The `client/registerCapability` request is sent from the server to the client to register for a new 50 | /// capability on the client side. Not all clients need to support dynamic capability registration. 51 | /// A client opts in via the dynamicRegistration property on the specific client capabilities. A client 52 | /// can even provide dynamic registration for capability A but not for capability B. 53 | abstract member ClientRegisterCapability: RegistrationParams -> AsyncLspResult 54 | 55 | default __.ClientRegisterCapability(_) = notImplemented 56 | 57 | /// The `client/unregisterCapability` request is sent from the server to the client to unregister a previously 58 | /// registered capability. 59 | abstract member ClientUnregisterCapability: UnregistrationParams -> AsyncLspResult 60 | 61 | default __.ClientUnregisterCapability(_) = notImplemented 62 | 63 | /// Many tools support more than one root folder per workspace. Examples for this are VS Code’s multi-root 64 | /// support, Atom’s project folder support or Sublime’s project support. If a client workspace consists of 65 | /// multiple roots then a server typically needs to know about this. The protocol up to know assumes one root 66 | /// folder which is announce to the server by the rootUri property of the InitializeParams. 67 | /// If the client supports workspace folders and announces them via the corresponding workspaceFolders client 68 | /// capability the InitializeParams contain an additional property workspaceFolders with the configured 69 | /// workspace folders when the server starts. 70 | /// 71 | /// The workspace/workspaceFolders request is sent from the server to the client to fetch the current open 72 | /// list of workspace folders. Returns null in the response if only a single file is open in the tool. 73 | /// Returns an empty array if a workspace is open but no folders are configured. 74 | abstract member WorkspaceWorkspaceFolders: unit -> AsyncLspResult 75 | 76 | default __.WorkspaceWorkspaceFolders() = notImplemented 77 | 78 | /// The workspace/configuration request is sent from the server to the client to fetch configuration 79 | /// settings from the client. 80 | /// 81 | /// The request can fetch n configuration settings in one roundtrip. The order of the returned configuration 82 | /// settings correspond to the order of the passed ConfigurationItems (e.g. the first item in the response 83 | /// is the result for the first configuration item in the params). 84 | abstract member WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult 85 | 86 | default __.WorkspaceConfiguration(_) = notImplemented 87 | 88 | abstract member WorkspaceApplyEdit: ApplyWorkspaceEditParams -> AsyncLspResult 89 | default __.WorkspaceApplyEdit(_) = notImplemented 90 | 91 | /// The workspace/semanticTokens/refresh request is sent from the server to the client. 92 | /// Servers can use it to ask clients to refresh the editors for which this server provides semantic tokens. 93 | /// As a result the client should ask the server to recompute the semantic tokens for these editors. 94 | /// This is useful if a server detects a project wide configuration change which requires a re-calculation 95 | /// of all semantic tokens. Note that the client still has the freedom to delay the re-calculation of 96 | /// the semantic tokens if for example an editor is currently not visible. 97 | abstract member WorkspaceSemanticTokensRefresh: unit -> AsyncLspResult 98 | 99 | default __.WorkspaceSemanticTokensRefresh() = notImplemented 100 | 101 | /// The `workspace/inlayHint/refresh` request is sent from the server to the client. 102 | /// Servers can use it to ask clients to refresh the inlay hints currently shown in editors. 103 | /// As a result the client should ask the server to recompute the inlay hints for these editors. 104 | /// This is useful if a server detects a configuration change which requires a re-calculation 105 | /// of all inlay hints. Note that the client still has the freedom to delay the re-calculation of the inlay hints 106 | /// if for example an editor is currently not visible. 107 | abstract member WorkspaceInlayHintRefresh: unit -> AsyncLspResult 108 | 109 | default __.WorkspaceInlayHintRefresh() = notImplemented 110 | 111 | /// The workspace/codeLens/refresh request is sent from the server to the client. Servers can use it to ask 112 | /// clients to refresh the code lenses currently shown in editors. As a result the client should ask the 113 | /// server to recompute the code lenses for these editors. This is useful if a server detects a 114 | /// configuration change which requires a re-calculation of all code lenses. Note that the client still has 115 | /// the freedom to delay the re-calculation of the code lenses if for example an editor is currently not 116 | /// visible. 117 | abstract member WorkspaceCodeLensRefresh: unit -> AsyncLspResult 118 | 119 | default __.WorkspaceCodeLensRefresh() = notImplemented 120 | 121 | /// The workspace/inlineValue/refresh request is sent from the server to the client. Servers can use it to 122 | /// ask clients to refresh the inline values currently shown in editors. As a result the client should ask 123 | /// the server to recompute the inline values for these editors. This is useful if a server detects a 124 | /// configuration change which requires a re-calculation of all inline values. Note that the client still 125 | /// has the freedom to delay the re-calculation of the inline values if for example an editor is currently 126 | /// not visible. 127 | abstract member WorkspaceInlineValueRefresh: unit -> AsyncLspResult 128 | 129 | default __.WorkspaceInlineValueRefresh() = notImplemented 130 | 131 | /// Diagnostics notification are sent from the server to the client to signal results of validation runs. 132 | /// 133 | /// Diagnostics are “owned” by the server so it is the server’s responsibility to clear them if necessary. 134 | /// The following rule is used for VS Code servers that generate diagnostics: 135 | /// 136 | /// * if a language is single file only (for example HTML) then diagnostics are cleared by the server when 137 | /// the file is closed. 138 | /// * if a language has a project system (for example C#) diagnostics are not cleared when a file closes. 139 | /// When a project is opened all diagnostics for all files are recomputed (or read from a cache). 140 | /// 141 | /// When a file changes it is the server’s responsibility to re-compute diagnostics and push them to the 142 | /// client. If the computed set is empty it has to push the empty array to clear former diagnostics. 143 | /// Newly pushed diagnostics always replace previously pushed diagnostics. There is no merging that happens 144 | /// on the client side. 145 | abstract member TextDocumentPublishDiagnostics: PublishDiagnosticsParams -> Async 146 | 147 | default __.TextDocumentPublishDiagnostics(_) = ignoreNotification 148 | 149 | /// The workspace/diagnostic/refresh request is sent from the server to the client. Servers can use it to 150 | /// ask clients to refresh all needed document and workspace diagnostics. This is useful if a server detects 151 | /// a project wide configuration change which requires a re-calculation of all diagnostics. 152 | abstract member WorkspaceDiagnosticRefresh: unit -> AsyncLspResult 153 | 154 | default __.WorkspaceDiagnosticRefresh() = notImplemented 155 | 156 | abstract member Progress: ProgressParams -> Async 157 | 158 | default __.Progress(p) = ignoreNotification 159 | 160 | abstract member CancelRequest: CancelParams -> Async 161 | default __.CancelRequest(_) = ignoreNotification 162 | 163 | abstract member LogTrace: LogTraceParams -> Async 164 | default __.LogTrace(_) = ignoreNotification 165 | 166 | /// The window/workDoneProgress/create request is sent from the server to the client to ask the client to create a work done progress. 167 | abstract member WindowWorkDoneProgressCreate: WorkDoneProgressCreateParams -> AsyncLspResult 168 | default __.WindowWorkDoneProgressCreate(_) = notImplemented 169 | 170 | interface ILspClient with 171 | member this.WindowShowMessage(p: ShowMessageParams) = this.WindowShowMessage(p) 172 | member this.WindowShowMessageRequest(p: ShowMessageRequestParams) = this.WindowShowMessageRequest(p) 173 | member this.WindowLogMessage(p: LogMessageParams) = this.WindowLogMessage(p) 174 | member this.WindowShowDocument(p: ShowDocumentParams) = this.WindowShowDocument(p) 175 | member this.TelemetryEvent(p: Newtonsoft.Json.Linq.JToken) = this.TelemetryEvent(p) 176 | member this.ClientRegisterCapability(p: RegistrationParams) = this.ClientRegisterCapability(p) 177 | member this.ClientUnregisterCapability(p: UnregistrationParams) = this.ClientUnregisterCapability(p) 178 | member this.WorkspaceWorkspaceFolders() = this.WorkspaceWorkspaceFolders() 179 | member this.WorkspaceConfiguration(p: ConfigurationParams) = this.WorkspaceConfiguration(p) 180 | member this.WorkspaceApplyEdit(p: ApplyWorkspaceEditParams) = this.WorkspaceApplyEdit(p) 181 | member this.WorkspaceSemanticTokensRefresh() = this.WorkspaceSemanticTokensRefresh() 182 | member this.WorkspaceInlayHintRefresh() = this.WorkspaceInlayHintRefresh() 183 | member this.WorkspaceCodeLensRefresh() = this.WorkspaceCodeLensRefresh() 184 | member this.WorkspaceInlineValueRefresh() = this.WorkspaceInlineValueRefresh() 185 | member this.TextDocumentPublishDiagnostics(p: PublishDiagnosticsParams) = this.TextDocumentPublishDiagnostics(p) 186 | member this.WorkspaceDiagnosticRefresh() = this.WorkspaceDiagnosticRefresh() 187 | member this.WindowWorkDoneProgressCreate(p: WorkDoneProgressCreateParams) = this.WindowWorkDoneProgressCreate(p) 188 | member this.Progress(p: ProgressParams) = this.Progress(p) 189 | member this.CancelRequest(p: CancelParams) : Async = this.CancelRequest(p) 190 | member this.LogTrace(p: LogTraceParams) : Async = this.LogTrace(p) 191 | member this.Dispose() : unit = () -------------------------------------------------------------------------------- /src/Ionide.LanguageServerProtocol.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | $(MSBuildThisFileDirectory)../CHANGELOG.md 7 | Library for implementing Language Server Protocol in F#. 8 | LSP, editor tooling 9 | chethusk; Krzysztof-Cieslak 10 | MIT 11 | README.md 12 | https://github.com/ionide/LanguageServerProtocol 13 | 3.17.0 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | <_MetaModelInputs 46 | Include="$(MSBuildThisFileDirectory)../data/$(LSPVersion)/metaModel.json" /> 47 | <_GenerationInputs 48 | Include="$(MSBuildThisFileDirectory)../tools/**/*.fs;$(MSBuildThisFileDirectory)../tools/**/*.fsproj" /> 49 | 50 | 51 | <_MetaModelOutputs Include="$(MSBuildThisFileDirectory)Types.cg.fs" /> 52 | <_MetaModelClientServerOutputs Include="$(MSBuildThisFileDirectory)ClientServer.cg.fs" /> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | <_GeneratorProject Include="../tools/MetaModelGenerator/MetaModelGenerator.fsproj" /> 68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | <_GenerateCommand Include="dotnet;@(_GeneratorApp)" /> 84 | <_GenerateCommand Include="types" /> 85 | <_GenerateCommand Include="--metamodelpath" /> 86 | <_GenerateCommand Include="%(_MetaModelInputs.FullPath)" /> 87 | <_GenerateCommand Include="--outputfilepath" /> 88 | <_GenerateCommand Include="%(_MetaModelOutputs.FullPath)" /> 89 | 90 | 91 | 93 | 94 | 95 | <_GenerateCommand2 Include="dotnet;@(_GeneratorApp)" /> 96 | <_GenerateCommand2 Include="clientserver" /> 97 | <_GenerateCommand2 Include="--metamodelpath" /> 98 | <_GenerateCommand2 Include="%(_MetaModelInputs.FullPath)" /> 99 | <_GenerateCommand2 Include="--outputfilepath" /> 100 | <_GenerateCommand2 Include="%(_MetaModelClientServerOutputs.FullPath)" /> 101 | 102 | 103 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/JsonRpc.fs: -------------------------------------------------------------------------------- 1 | module Ionide.LanguageServerProtocol.JsonRpc 2 | 3 | open Newtonsoft.Json 4 | open Newtonsoft.Json.Linq 5 | 6 | type MessageTypeTest = { 7 | [] 8 | Version: string 9 | Id: int option 10 | Method: string option 11 | } 12 | 13 | [] 14 | type MessageType = 15 | | Notification 16 | | Request 17 | | Response 18 | | Error 19 | 20 | let getMessageType messageTest = 21 | match messageTest with 22 | | { Version = "2.0"; Id = Some _; Method = Some _ } -> MessageType.Request 23 | | { Version = "2.0"; Id = Some _; Method = None } -> MessageType.Response 24 | | { Version = "2.0"; Id = None; Method = Some _ } -> MessageType.Notification 25 | | _ -> MessageType.Error 26 | 27 | type Request = { 28 | [] 29 | Version: string 30 | Id: int 31 | Method: string 32 | Params: JToken option 33 | } with 34 | 35 | static member Create(id: int, method': string, rpcParams: JToken option) = { 36 | Version = "2.0" 37 | Id = id 38 | Method = method' 39 | Params = rpcParams 40 | } 41 | 42 | type Notification = { 43 | [] 44 | Version: string 45 | Method: string 46 | Params: JToken option 47 | } with 48 | 49 | static member Create(method': string, rpcParams: JToken option) = { 50 | Version = "2.0" 51 | Method = method' 52 | Params = rpcParams 53 | } 54 | 55 | module ErrorCodes = 56 | ///This is the start range of JSON-RPC reserved error codes. 57 | ///It doesn't denote a real error code. No LSP error codes should 58 | ///be defined between the start and end range. For backwards 59 | ///compatibility the and the 60 | ///are left in the range. 61 | let jsonrpcReservedErrorRangeStart = -32099 62 | ///This is the end range of JSON-RPC reserved error codes. It doesn't denote a real error code. 63 | let jsonrpcReservedErrorRangeEnd = -32000 64 | 65 | /// This is the start range of LSP reserved error codes. It doesn't denote a real error code. 66 | let lspReservedErrorRangeStart = -32899 67 | /// This is the end range of LSP reserved error codes. It doesn't denote a real error code. 68 | let lspReservedErrorRangeEnd = -32899 69 | 70 | open Ionide.LanguageServerProtocol.Types 71 | 72 | type Error = { 73 | Code: int 74 | Message: string 75 | Data: JToken option 76 | } with 77 | 78 | static member Create(code: int, message: string) = { Code = code; Message = message; Data = None } 79 | 80 | static member ParseError(?message) = 81 | let message = defaultArg message "Parse error" 82 | Error.Create(int ErrorCodes.ParseError, message) 83 | 84 | static member InvalidRequest(?message) = 85 | let message = defaultArg message "Invalid Request" 86 | Error.Create(int ErrorCodes.InvalidRequest, message) 87 | 88 | static member MethodNotFound(?message) = 89 | let message = defaultArg message "Method not found" 90 | Error.Create(int ErrorCodes.MethodNotFound, message) 91 | 92 | static member InvalidParams(?message) = 93 | let message = defaultArg message "Invalid params" 94 | Error.Create(int ErrorCodes.InvalidParams, message) 95 | 96 | static member InternalError(?message: string) = 97 | let message = defaultArg message "Internal error" 98 | Error.Create(int ErrorCodes.InternalError, message) 99 | 100 | static member RequestCancelled(?message) = 101 | let message = defaultArg message "Request cancelled" 102 | Error.Create(int LSPErrorCodes.RequestCancelled, message) 103 | 104 | type Response = { 105 | [] 106 | Version: string 107 | Id: int option 108 | Error: Error option 109 | [] 110 | Result: JToken option 111 | } with 112 | 113 | /// Json.NET conditional property serialization, controlled by naming convention 114 | member x.ShouldSerializeResult() = x.Error.IsNone 115 | 116 | static member Success(id: int, result: JToken option) = { 117 | Version = "2.0" 118 | Id = Some id 119 | Result = result 120 | Error = None 121 | } 122 | 123 | static member Failure(id: int, error: Error) = { Version = "2.0"; Id = Some id; Result = None; Error = Some error } 124 | 125 | 126 | /// Result type composed of a success value or an error of type JsonRpc.Error 127 | type LspResult<'t> = Result<'t, Error> 128 | /// Async Result type composed of a success value or an error of type JsonRpc.Error 129 | type AsyncLspResult<'t> = Async> 130 | 131 | 132 | module LspResult = 133 | 134 | let success x : LspResult<_> = Result.Ok x 135 | 136 | let invalidParams message : LspResult<_> = Result.Error(Error.InvalidParams message) 137 | 138 | let internalError<'a> (message: string) : LspResult<'a> = 139 | Result.Error(Error.Create(int ErrorCodes.InvalidParams, message)) 140 | 141 | let notImplemented<'a> : LspResult<'a> = Result.Error(Error.MethodNotFound()) 142 | 143 | let requestCancelled<'a> : LspResult<'a> = Result.Error(Error.RequestCancelled()) 144 | 145 | module AsyncLspResult = 146 | 147 | let success x : AsyncLspResult<_> = async.Return(Result.Ok x) 148 | 149 | let invalidParams message : AsyncLspResult<_> = async.Return(LspResult.invalidParams message) 150 | 151 | let internalError message : AsyncLspResult<_> = async.Return(LspResult.internalError message) 152 | 153 | let notImplemented<'a> : AsyncLspResult<'a> = async.Return LspResult.notImplemented 154 | 155 | let requestCancelled<'a> : AsyncLspResult<'a> = async.Return LspResult.requestCancelled 156 | 157 | 158 | module Requests = 159 | open StreamJsonRpc 160 | open System 161 | open System.Threading 162 | open System.Threading.Tasks 163 | 164 | let requestHandling<'param, 'result> (run: 'param -> AsyncLspResult<'result>) : Delegate = 165 | let runAsTask param ct = 166 | // Execute non-async portion of `run` before forking the async portion into a task. 167 | // This is needed to avoid reordering of messages from a client. 168 | let asyncLspResult = run param 169 | 170 | let asyncContinuation = 171 | async { 172 | let! lspResult = asyncLspResult 173 | 174 | return 175 | match lspResult with 176 | | Ok result -> result 177 | | Error error -> 178 | let rpcException = LocalRpcException(error.Message) 179 | rpcException.ErrorCode <- error.Code 180 | 181 | rpcException.ErrorData <- 182 | error.Data 183 | |> Option.defaultValue null 184 | 185 | raise rpcException 186 | } 187 | 188 | Async.StartAsTask(asyncContinuation, cancellationToken = ct) 189 | 190 | Func<'param, CancellationToken, Task<'result>>(runAsTask) :> Delegate 191 | 192 | /// Notifications don't generate a response or error, but to unify things we consider them as always successful. 193 | /// They will still not send any response because their ID is null. 194 | let internal notificationSuccess (response: Async) = 195 | async { 196 | do! response 197 | return Result.Ok() 198 | } -------------------------------------------------------------------------------- /src/JsonUtils.fs: -------------------------------------------------------------------------------- 1 | namespace Ionide.LanguageServerProtocol.JsonUtils 2 | 3 | open Microsoft.FSharp.Reflection 4 | open Newtonsoft.Json 5 | open System 6 | open System.Collections.Concurrent 7 | open Ionide.LanguageServerProtocol.Types 8 | open Newtonsoft.Json.Linq 9 | open Newtonsoft.Json.Serialization 10 | open System.Reflection 11 | open Converters 12 | 13 | 14 | /// Handles fields of type `Option`: 15 | /// * Allows missing json properties when `Option` -> Optional 16 | /// * Fails when missing json property when not `Option` -> Required 17 | /// * Additional properties in json are always ignored 18 | /// 19 | /// Example: 20 | /// ```fsharp 21 | /// type Data = { Name: string; Value: int option } 22 | /// ``` 23 | /// ```json 24 | /// { "name": "foo", "value": 42 } // ok 25 | /// { "name": "foo" } // ok 26 | /// { "value": 42 } // error 27 | /// {} // error 28 | /// { "name": "foo", "data": "bar" } // ok 29 | /// ``` 30 | [] 31 | type OptionAndCamelCasePropertyNamesContractResolver() as this = 32 | inherit CamelCasePropertyNamesContractResolver() 33 | 34 | do this.NamingStrategy.ProcessDictionaryKeys <- false 35 | 36 | let isOptionType (ty: Type) = 37 | ty.IsGenericType 38 | && ty.GetGenericTypeDefinition() = typedefof> 39 | 40 | override _.CreateProperty(memberInfo, memberSerialization) = 41 | // mutable properties in records have their corresponding field deserialized too 42 | // field has postfix `@` 43 | // -> exclude everything ending in `@` (-> ~ private fields) 44 | if memberInfo.Name.EndsWith "@" then 45 | null 46 | else 47 | let prop = ``base``.CreateProperty(memberInfo, memberSerialization) 48 | 49 | let shouldUpdateRequired = 50 | // change nothing when specified: 51 | // * `JsonProperty.Required` 52 | // * Don't know if specified -> compare with `Default` 53 | match memberInfo.GetCustomAttribute() with 54 | | null -> true 55 | | jp -> jp.Required = Required.Default 56 | 57 | if shouldUpdateRequired then 58 | if Type.isOption prop.PropertyType then 59 | prop.Required <- Required.Default 60 | else 61 | prop.Required <- Required.Always 62 | 63 | prop 64 | 65 | 66 | /// Newtonsoft.Json parses parses a number inside quotations as number too: 67 | /// `"42"` -> can be parsed to `42: int` 68 | /// This converter prevents that. `"42"` cannot be parsed to `int` (or `float`) any more 69 | [] 70 | type StrictNumberConverter() = 71 | inherit JsonConverter() 72 | 73 | static let defaultSerializer = JsonSerializer() 74 | 75 | override _.CanConvert(t) = 76 | t 77 | |> Type.isNumeric 78 | 79 | override __.ReadJson(reader, t, _, serializer) = 80 | match reader.TokenType with 81 | | JsonToken.Integer 82 | | JsonToken.Float -> 83 | // cannot use `serializer`: Endless recursion into StrictNumberConverter for same value 84 | defaultSerializer.Deserialize(reader, t) 85 | | _ -> failwith $"Expected a number, but was {reader.TokenType}" 86 | 87 | override _.CanWrite = false 88 | override _.WriteJson(_, _, _) = raise (NotImplementedException()) 89 | 90 | /// Like `StrictNumberConverter`, but prevents numbers to be parsed as string: 91 | /// `42` -> no quotation marks -> not a string 92 | [] 93 | type StrictStringConverter() = 94 | inherit JsonConverter() 95 | 96 | override _.CanConvert(t) = 97 | t 98 | |> Type.isString 99 | 100 | override __.ReadJson(reader, t, _, serializer) = 101 | match reader.TokenType with 102 | | JsonToken.String -> reader.Value 103 | | JsonToken.Null -> null 104 | | _ -> failwith $"Expected a string, but was {reader.TokenType}" 105 | 106 | override _.CanWrite = false 107 | override _.WriteJson(_, _, _) = raise (NotImplementedException()) 108 | 109 | /// Like `StrictNumberConverter`, but prevents boolean to be parsed as string: 110 | /// `true` -> no quotation marks -> not a string 111 | [] 112 | type StrictBoolConverter() = 113 | inherit JsonConverter() 114 | 115 | override _.CanConvert(t) = 116 | t 117 | |> Type.isBool 118 | 119 | override __.ReadJson(reader, t, _, serializer) = 120 | match reader.TokenType with 121 | | JsonToken.Boolean -> reader.Value 122 | | _ -> failwith $"Expected a bool, but was {reader.TokenType}" 123 | 124 | override _.CanWrite = false 125 | override _.WriteJson(_, _, _) = raise (NotImplementedException()) 126 | 127 | [] 128 | type ErasedUnionConverter() = 129 | inherit JsonConverter() 130 | 131 | let canConvert = 132 | memoriseByHash (fun t -> 133 | FSharpType.IsUnion t 134 | && ( 135 | // Union 136 | t.GetCustomAttributes(typedefof, false).Length > 0 137 | || 138 | // Case 139 | t.BaseType.GetCustomAttributes(typedefof, false).Length > 0) 140 | ) 141 | 142 | override __.CanConvert(t) = canConvert t 143 | 144 | override __.WriteJson(writer, value, serializer) = 145 | let union = UnionInfo.get (value.GetType()) 146 | let case = union.GetCaseOf value 147 | // Must be exactly 1 field 148 | // Deliberately fail here to signal incorrect usage 149 | // (vs. `CanConvert` = `false` -> silent and fallback to serialization with `case` & `fields`) 150 | match case.GetFieldValues value with 151 | | [| value |] -> serializer.Serialize(writer, value) 152 | | values -> failwith $"Expected exactly one field for case `{value.GetType().Name}`, but were {values.Length}" 153 | 154 | override __.ReadJson(reader: JsonReader, t, _existingValue, serializer) = 155 | let tryReadPrimitive (json: JToken) (targetType: Type) = 156 | if Type.isString targetType then 157 | if json.Type = JTokenType.String then 158 | reader.Value 159 | |> Some 160 | else 161 | None 162 | elif Type.isBool targetType then 163 | if json.Type = JTokenType.Boolean then 164 | reader.Value 165 | |> Some 166 | else 167 | None 168 | elif Type.isNumeric targetType then 169 | match json.Type with 170 | | JTokenType.Integer 171 | | JTokenType.Float -> 172 | json.ToObject(targetType, serializer) 173 | |> Some 174 | | _ -> None 175 | else 176 | None 177 | 178 | let tryReadUnionKind (json: JToken) (targetType: Type) = 179 | try 180 | let fields = json.Children() 181 | let props = targetType.GetProperties() 182 | 183 | match 184 | fields 185 | |> Seq.tryFind (fun f -> f.Name.ToLowerInvariant() = "kind"), 186 | props 187 | |> Seq.tryFind (fun p -> p.Name.ToLowerInvariant() = "kind") 188 | with 189 | | Some f, Some p -> 190 | match 191 | p.GetCustomAttribute(typeof) 192 | |> Option.ofObj 193 | with 194 | | Some(:? UnionKindAttribute as k) when k.Value = string f.Value -> 195 | json.ToObject(targetType, serializer) 196 | |> Some 197 | | _ -> None 198 | | _ -> None 199 | with _ -> 200 | None 201 | 202 | let tryReadAllMatchingFields (json: JToken) (targetType: Type) = 203 | try 204 | let fields = 205 | json.Children() 206 | |> Seq.map (fun f -> f.Name.ToLowerInvariant()) 207 | 208 | let props = 209 | targetType.GetProperties() 210 | |> Seq.map (fun p -> p.Name.ToLowerInvariant()) 211 | 212 | if 213 | fields 214 | |> Seq.forall (fun f -> 215 | props 216 | |> Seq.contains f 217 | ) 218 | then 219 | json.ToObject(targetType, serializer) 220 | |> Some 221 | else 222 | None 223 | with _ -> 224 | None 225 | 226 | let union = UnionInfo.get t 227 | let json = JToken.ReadFrom reader 228 | 229 | let tryMakeUnionCase tryReadValue (json: JToken) (case: CaseInfo) = 230 | match case.Fields with 231 | | [| field |] -> 232 | let ty = field.PropertyType 233 | 234 | match tryReadValue json ty with 235 | | None -> None 236 | | Some value -> 237 | case.Create [| value |] 238 | |> Some 239 | | fields -> 240 | failwith 241 | $"Expected union {case.Info.DeclaringType.Name} to have exactly one field in each case, but case {case.Info.Name} has {fields.Length} fields" 242 | 243 | let c = 244 | union.Cases 245 | |> Array.tryPick (tryMakeUnionCase tryReadPrimitive json) 246 | |> Option.orElseWith (fun () -> 247 | union.Cases 248 | |> Array.tryPick (tryMakeUnionCase tryReadUnionKind json) 249 | ) 250 | |> Option.orElseWith (fun () -> 251 | union.Cases 252 | |> Array.tryPick (tryMakeUnionCase tryReadAllMatchingFields json) 253 | ) 254 | 255 | match c with 256 | | None -> failwith $"Could not create an instance of the type '%s{t.Name}'" 257 | | Some c -> c 258 | 259 | /// converter that can convert enum-style DUs 260 | [] 261 | type SingleCaseUnionConverter() = 262 | inherit JsonConverter() 263 | 264 | let canConvert = 265 | let allCases (t: System.Type) = FSharpType.GetUnionCases t 266 | 267 | memoriseByHash (fun t -> 268 | FSharpType.IsUnion t 269 | && allCases t 270 | |> Array.forall (fun c -> c.GetFields().Length = 0) 271 | ) 272 | 273 | override _.CanConvert t = canConvert t 274 | 275 | override _.WriteJson(writer: Newtonsoft.Json.JsonWriter, value: obj, serializer: Newtonsoft.Json.JsonSerializer) = 276 | serializer.Serialize(writer, string value) 277 | 278 | override _.ReadJson(reader: Newtonsoft.Json.JsonReader, t, _existingValue, serializer) = 279 | let caseName = string reader.Value 280 | 281 | let union = UnionInfo.get t 282 | 283 | let case = 284 | union.Cases 285 | |> Array.tryFind (fun c -> c.Info.Name.Equals(caseName, StringComparison.OrdinalIgnoreCase)) 286 | 287 | match case with 288 | | Some case -> case.Create [||] 289 | | None -> failwith $"Could not create an instance of the type '%s{t.Name}' with the name '%s{caseName}'" -------------------------------------------------------------------------------- /src/LanguageServerProtocol.fs: -------------------------------------------------------------------------------- 1 | namespace Ionide.LanguageServerProtocol 2 | 3 | module Server = 4 | open System 5 | open System.IO 6 | open Ionide.LanguageServerProtocol.Logging 7 | open Ionide.LanguageServerProtocol.Types 8 | open System.Threading 9 | open System.Threading.Tasks 10 | open System.Reflection 11 | open StreamJsonRpc 12 | open Newtonsoft.Json 13 | open Ionide.LanguageServerProtocol.JsonUtils 14 | open Newtonsoft.Json.Linq 15 | open StreamJsonRpc 16 | open StreamJsonRpc.Protocol 17 | open JsonRpc 18 | open Mappings 19 | 20 | let logger = LogProvider.getLoggerByName "LSP Server" 21 | 22 | let defaultJsonRpcFormatter () = 23 | let jsonRpcFormatter = new JsonMessageFormatter() 24 | jsonRpcFormatter.JsonSerializer.NullValueHandling <- NullValueHandling.Ignore 25 | jsonRpcFormatter.JsonSerializer.ConstructorHandling <- ConstructorHandling.AllowNonPublicDefaultConstructor 26 | jsonRpcFormatter.JsonSerializer.MissingMemberHandling <- MissingMemberHandling.Ignore 27 | jsonRpcFormatter.JsonSerializer.Converters.Add(StrictNumberConverter()) 28 | jsonRpcFormatter.JsonSerializer.Converters.Add(StrictStringConverter()) 29 | jsonRpcFormatter.JsonSerializer.Converters.Add(StrictBoolConverter()) 30 | jsonRpcFormatter.JsonSerializer.Converters.Add(SingleCaseUnionConverter()) 31 | jsonRpcFormatter.JsonSerializer.Converters.Add(OptionConverter()) 32 | jsonRpcFormatter.JsonSerializer.Converters.Add(ErasedUnionConverter()) 33 | jsonRpcFormatter.JsonSerializer.ContractResolver <- OptionAndCamelCasePropertyNamesContractResolver() 34 | jsonRpcFormatter 35 | 36 | let jsonRpcFormatter = defaultJsonRpcFormatter () 37 | 38 | let deserialize<'t> (token: JToken) = token.ToObject<'t>(jsonRpcFormatter.JsonSerializer) 39 | let serialize<'t> (o: 't) = JToken.FromObject(o, jsonRpcFormatter.JsonSerializer) 40 | 41 | let requestHandling<'param, 'result> (run: 'param -> AsyncLspResult<'result>) : Delegate = 42 | let runAsTask param ct = 43 | // Execute non-async portion of `run` before forking the async portion into a task. 44 | // This is needed to avoid reordering of messages from a client. 45 | let asyncLspResult = run param 46 | 47 | let asyncContinuation = 48 | async { 49 | let! lspResult = asyncLspResult 50 | 51 | return 52 | match lspResult with 53 | | Ok result -> result 54 | | Error error -> 55 | let rpcException = LocalRpcException(error.Message) 56 | rpcException.ErrorCode <- error.Code 57 | 58 | rpcException.ErrorData <- 59 | error.Data 60 | |> Option.defaultValue null 61 | 62 | raise rpcException 63 | } 64 | 65 | Async.StartAsTask(asyncContinuation, cancellationToken = ct) 66 | 67 | Func<'param, CancellationToken, Task<'result>>(runAsTask) :> Delegate 68 | 69 | /// Notifications don't generate a response or error, but to unify things we consider them as always successful. 70 | /// They will still not send any response because their ID is null. 71 | let private notificationSuccess (response: Async) = 72 | async { 73 | do! response 74 | return Result.Ok() 75 | } 76 | 77 | type ClientNotificationSender = string -> obj -> AsyncLspResult 78 | 79 | type ClientRequestSender = 80 | abstract member Send<'a> : string -> obj -> AsyncLspResult<'a> 81 | 82 | type private MessageHandlingResult = 83 | | Normal 84 | | WasExit 85 | | WasShutdown 86 | 87 | type LspCloseReason = 88 | | RequestedByClient = 0 89 | | ErrorExitWithoutShutdown = 1 90 | | ErrorStreamClosed = 2 91 | 92 | let (|Flatten|) (ex: Exception) : Exception = 93 | match ex with 94 | | :? AggregateException as aex -> 95 | let aex = aex.Flatten() 96 | 97 | if aex.InnerExceptions.Count = 1 then 98 | aex.InnerException 99 | else 100 | aex 101 | | _ -> ex 102 | 103 | 104 | /// The default RPC logic shipped with this library. All this does is mark LocalRpcExceptions as non-fatal 105 | let defaultRpc (handler: IJsonRpcMessageHandler) = 106 | { new JsonRpc(handler) with 107 | member this.IsFatalException(ex: Exception) = 108 | match ex with 109 | | Flatten(:? LocalRpcException | :? JsonSerializationException) -> false 110 | | _ -> true 111 | 112 | member this.CreateErrorDetails(request: JsonRpcRequest, ex: Exception) = 113 | let isSerializable = this.ExceptionStrategy = ExceptionProcessing.ISerializable 114 | 115 | match ex with 116 | | Flatten(:? JsonSerializationException as ex) -> 117 | let data: obj = if isSerializable then ex else CommonErrorData(ex) 118 | JsonRpcError.ErrorDetail(Code = JsonRpcErrorCode.ParseError, Message = ex.Message, Data = data) 119 | | _ -> ``base``.CreateErrorDetails(request, ex) 120 | } 121 | 122 | let startWithSetupCore<'client when 'client :> Ionide.LanguageServerProtocol.ILspClient> 123 | (setupRequestHandlings: 'client -> Map) 124 | (jsonRpcHandler: IJsonRpcMessageHandler) 125 | (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) 126 | (customizeRpc: IJsonRpcMessageHandler -> JsonRpc) 127 | = 128 | // Without overriding isFatalException, JsonRpc serializes exceptions and sends them to the client. 129 | // This is particularly bad for notifications such as textDocument/didChange which don't require a response, 130 | // and thus any exception that happens during e.g. text sync gets swallowed. 131 | use jsonRpc = customizeRpc jsonRpcHandler 132 | 133 | /// When the server wants to send a notification to the client 134 | let sendServerNotification (rpcMethod: string) (notificationObj: obj) : AsyncLspResult = 135 | async { 136 | do! 137 | jsonRpc.NotifyWithParameterObjectAsync(rpcMethod, notificationObj) 138 | |> Async.AwaitTask 139 | 140 | return 141 | () 142 | |> LspResult.success 143 | } 144 | 145 | /// When the server wants to send a request to the client 146 | let sendServerRequest (rpcMethod: string) (requestObj: obj) : AsyncLspResult<'response> = 147 | async { 148 | let! response = 149 | jsonRpc.InvokeWithParameterObjectAsync<'response>(rpcMethod, requestObj) 150 | |> Async.AwaitTask 151 | 152 | return 153 | response 154 | |> LspResult.success 155 | } 156 | 157 | let lspClient = 158 | clientCreator ( 159 | sendServerNotification, 160 | { new ClientRequestSender with 161 | member __.Send x t = sendServerRequest x t 162 | } 163 | ) 164 | 165 | // Note on server shutdown. 166 | // According the the LSP spec the shutdown sequence consists fo a client sending onShutdown request followed by 167 | // onExit notification. The server can terminate after receiving onExit. However, real language clients implements 168 | // the shutdown in their own way: 169 | // 1. VSCode Language Client has a bug that causes it to NOT send an `exit` notification when stopping a server: 170 | // https://github.com/microsoft/vscode-languageserver-node/pull/776 171 | // VSCode sends onShutdown and then closes the connection. 172 | // 2. Neovim LSP sends onShutdown followed by onExit but does NOT close the connection on its own. 173 | // 3. Emacs LSP mode sends onShutdown followed by onExit and then closes the connection. 174 | // This is the reason for the complicated logic below. 175 | 176 | let mutable shutdownReceived = false 177 | let mutable quitReceived = false 178 | use quitSemaphore = new SemaphoreSlim(0, 1) 179 | 180 | let onShutdown () = 181 | logger.trace (Log.setMessage "Shutdown received") 182 | shutdownReceived <- true 183 | 184 | jsonRpc.AddLocalRpcMethod("shutdown", Action(onShutdown)) 185 | 186 | let onExit () = 187 | logger.trace (Log.setMessage "Exit received") 188 | quitReceived <- true 189 | 190 | quitSemaphore.Release() 191 | |> ignore 192 | 193 | jsonRpc.AddLocalRpcMethod("exit", Action(onExit)) 194 | 195 | for handling in setupRequestHandlings lspClient do 196 | let rpcMethodName = handling.Key 197 | let rpcDelegate = handling.Value 198 | 199 | let rpcAttribute = JsonRpcMethodAttribute(rpcMethodName) 200 | rpcAttribute.UseSingleObjectParameterDeserialization <- true 201 | 202 | jsonRpc.AddLocalRpcMethod(rpcDelegate.GetMethodInfo(), rpcDelegate.Target, rpcAttribute) 203 | 204 | jsonRpc.StartListening() 205 | 206 | // 1. jsonRpc.Completion finishes when either a connection is closed or a fatal exception is thrown. 207 | // 2. quitSemaphore is released when the server receives both onShutdown and onExit. 208 | // Completion of either of those causes the server to stop. 209 | let completed_task_idx = Task.WaitAny(jsonRpc.Completion, quitSemaphore.WaitAsync()) 210 | // jsonRpc.Completion throws on fatal exception. However, Task.WaitAny doesn't even when jsonRpc.Completion would. 211 | // Here we check and re-raise if needed. 212 | if completed_task_idx = 0 then 213 | match jsonRpc.Completion.Exception with 214 | | null -> () 215 | | exn -> raise exn 216 | 217 | match shutdownReceived, quitReceived with 218 | | true, true -> LspCloseReason.RequestedByClient 219 | | false, true -> LspCloseReason.ErrorExitWithoutShutdown 220 | | _ -> LspCloseReason.ErrorStreamClosed 221 | 222 | let startWithSetup<'client when 'client :> Ionide.LanguageServerProtocol.ILspClient> 223 | (setupRequestHandlings: 'client -> Map) 224 | (input: Stream) 225 | (output: Stream) 226 | (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) 227 | (customizeRpc: IJsonRpcMessageHandler -> JsonRpc) 228 | = 229 | use jsonRpcHandler = new HeaderDelimitedMessageHandler(output, input, defaultJsonRpcFormatter ()) 230 | startWithSetupCore setupRequestHandlings jsonRpcHandler clientCreator customizeRpc 231 | 232 | let startWithSetupWs<'client when 'client :> Ionide.LanguageServerProtocol.ILspClient> 233 | (setupRequestHandlings: 'client -> Map) 234 | (socket: System.Net.WebSockets.WebSocket) 235 | (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) 236 | (customizeRpc: IJsonRpcMessageHandler -> JsonRpc) 237 | = 238 | use jsonRpcHandler = new WebSocketMessageHandler(socket, defaultJsonRpcFormatter ()) 239 | startWithSetupCore setupRequestHandlings jsonRpcHandler clientCreator customizeRpc 240 | 241 | 242 | let serverRequestHandling<'server, 'param, 'result when 'server :> Ionide.LanguageServerProtocol.ILspServer> 243 | (run: 'server -> 'param -> AsyncLspResult<'result>) 244 | : ServerRequestHandling<'server> = 245 | { Run = fun s -> requestHandling (run s) } 246 | 247 | let defaultRequestHandlings () : Map> = 248 | routeMappings () 249 | |> Map.ofList 250 | 251 | let private requestHandlingSetupFunc<'client, 'server 252 | when 'client :> Ionide.LanguageServerProtocol.ILspClient and 'server :> Ionide.LanguageServerProtocol.ILspServer> 253 | (requestHandlings: Map>) 254 | (serverCreator: 'client -> 'server) 255 | = 256 | fun client -> 257 | let server = serverCreator client 258 | 259 | requestHandlings 260 | |> Map.map (fun _ requestHandling -> requestHandling.Run server) 261 | 262 | let start<'client, 'server 263 | when 'client :> Ionide.LanguageServerProtocol.ILspClient and 'server :> Ionide.LanguageServerProtocol.ILspServer> 264 | (requestHandlings: Map>) 265 | (input: Stream) 266 | (output: Stream) 267 | (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) 268 | (serverCreator: 'client -> 'server) 269 | = 270 | let requestHandlingSetup = requestHandlingSetupFunc requestHandlings serverCreator 271 | startWithSetup requestHandlingSetup input output clientCreator 272 | 273 | let startWs<'client, 'server 274 | when 'client :> Ionide.LanguageServerProtocol.ILspClient and 'server :> Ionide.LanguageServerProtocol.ILspServer> 275 | (requestHandlings: Map>) 276 | (socket: Net.WebSockets.WebSocket) 277 | (clientCreator: (ClientNotificationSender * ClientRequestSender) -> 'client) 278 | (serverCreator: 'client -> 'server) 279 | = 280 | let requestHandlingSetup = requestHandlingSetupFunc requestHandlings serverCreator 281 | startWithSetupWs requestHandlingSetup socket clientCreator 282 | 283 | module Client = 284 | open System 285 | open System.Diagnostics 286 | open System.IO 287 | open Ionide.LanguageServerProtocol 288 | open Ionide.LanguageServerProtocol.JsonRpc 289 | open Ionide.LanguageServerProtocol.Logging 290 | open Ionide.LanguageServerProtocol.JsonUtils 291 | open Newtonsoft.Json 292 | open Newtonsoft.Json.Serialization 293 | open Newtonsoft.Json.Linq 294 | 295 | 296 | let logger = LogProvider.getLoggerByName "LSP Client" 297 | 298 | let internal jsonSettings = 299 | let result = JsonSerializerSettings(NullValueHandling = NullValueHandling.Ignore) 300 | result.Converters.Add(OptionConverter()) 301 | result.Converters.Add(ErasedUnionConverter()) 302 | result.ContractResolver <- CamelCasePropertyNamesContractResolver() 303 | result 304 | 305 | let internal jsonSerializer = JsonSerializer.Create(jsonSettings) 306 | 307 | let internal deserialize (token: JToken) = token.ToObject<'t>(jsonSerializer) 308 | 309 | let internal serialize (o: 't) = JToken.FromObject(o, jsonSerializer) 310 | 311 | type NotificationHandler = { Run: JToken -> Async } 312 | 313 | let notificationHandling<'p, 'r> (handler: 'p -> Async<'r option>) : NotificationHandler = 314 | let run (token: JToken) = 315 | async { 316 | try 317 | let p = token.ToObject<'p>(jsonSerializer) 318 | let! res = handler p 319 | 320 | return 321 | res 322 | |> Option.map (fun n -> JToken.FromObject(n, jsonSerializer)) 323 | with _ -> 324 | return None 325 | } 326 | 327 | { Run = run } 328 | 329 | // TODO: replace this module with StreamJsonRpc like we did for the server 330 | module LowLevel = 331 | open System 332 | open System.IO 333 | open System.Text 334 | 335 | let headerBufferSize = 300 336 | let minimumHeaderLength = 21 337 | let cr = byte '\r' 338 | let lf = byte '\f' 339 | let headerEncoding = Encoding.ASCII 340 | 341 | let private readLine (stream: Stream) = 342 | let buffer = Array.zeroCreate headerBufferSize 343 | let readCount = stream.Read(buffer, 0, 2) 344 | let mutable count = readCount 345 | 346 | if count < 2 then 347 | None 348 | else 349 | // TODO: Check that we don't over-fill headerBufferSize 350 | while count < headerBufferSize 351 | && (buffer.[count - 2] 352 | <> cr 353 | && buffer.[count - 1] 354 | <> lf) do 355 | let additionalBytesRead = stream.Read(buffer, count, 1) 356 | // TODO: exit when additionalBytesRead = 0, end of stream 357 | count <- 358 | count 359 | + additionalBytesRead 360 | 361 | if 362 | count 363 | >= headerBufferSize 364 | then 365 | None 366 | else 367 | Some(headerEncoding.GetString(buffer, 0, count - 2)) 368 | 369 | let rec private readHeaders (stream: Stream) = 370 | let line = readLine stream 371 | 372 | match line with 373 | | Some "" -> [] 374 | | Some line -> 375 | let separatorPos = line.IndexOf(": ") 376 | 377 | if separatorPos = -1 then 378 | raise (Exception(sprintf "Separator not found in header '%s'" line)) 379 | else 380 | let name = line.Substring(0, separatorPos) 381 | 382 | let value = 383 | line.Substring( 384 | separatorPos 385 | + 2 386 | ) 387 | 388 | let otherHeaders = readHeaders stream 389 | 390 | (name, value) 391 | :: otherHeaders 392 | | None -> raise (EndOfStreamException()) 393 | 394 | let read (stream: Stream) = 395 | let headers = readHeaders stream 396 | 397 | let contentLength = 398 | headers 399 | |> List.tryFind (fun (name, _) -> name = "Content-Length") 400 | |> Option.map snd 401 | |> Option.bind (fun s -> 402 | match Int32.TryParse(s) with 403 | | true, x -> Some x 404 | | _ -> None 405 | ) 406 | 407 | if contentLength = None then 408 | failwithf "Content-Length header not found" 409 | else 410 | let result = Array.zeroCreate contentLength.Value 411 | let mutable readCount = 0 412 | 413 | while readCount < contentLength.Value do 414 | let toRead = 415 | contentLength.Value 416 | - readCount 417 | 418 | let readInCurrentBatch = stream.Read(result, readCount, toRead) 419 | 420 | readCount <- 421 | readCount 422 | + readInCurrentBatch 423 | 424 | let str = Encoding.UTF8.GetString(result, 0, readCount) 425 | headers, str 426 | 427 | let write (stream: Stream) (data: string) = 428 | let bytes = Encoding.UTF8.GetBytes(data) 429 | 430 | let header = 431 | sprintf "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\nContent-Length: %d\r\n\r\n" bytes.Length 432 | 433 | let headerBytes = Encoding.ASCII.GetBytes header 434 | 435 | use ms = 436 | new MemoryStream( 437 | headerBytes.Length 438 | + bytes.Length 439 | ) 440 | 441 | ms.Write(headerBytes, 0, headerBytes.Length) 442 | ms.Write(bytes, 0, bytes.Length) 443 | stream.Write(ms.ToArray(), 0, int ms.Position) 444 | 445 | type Client(exec: string, args: string, notificationHandlings: Map) = 446 | 447 | let mutable outuptStream: StreamReader option = None 448 | let mutable inputStream: StreamWriter option = None 449 | 450 | let sender = 451 | MailboxProcessor.Start(fun inbox -> 452 | let rec loop () = 453 | async { 454 | let! str = inbox.Receive() 455 | 456 | inputStream 457 | |> Option.iter (fun input -> 458 | // fprintfn stderr "[CLIENT] Writing: %s" str 459 | LowLevel.write input.BaseStream str 460 | input.BaseStream.Flush() 461 | ) 462 | // do! Async.Sleep 1000 463 | return! loop () 464 | } 465 | 466 | loop () 467 | ) 468 | 469 | let handleRequest (request: JsonRpc.Request) = 470 | async { 471 | let mutable methodCallResult = None 472 | 473 | match 474 | notificationHandlings 475 | |> Map.tryFind request.Method 476 | with 477 | | Some handling -> 478 | try 479 | match request.Params with 480 | | None -> () 481 | | Some prms -> 482 | let! result = handling.Run prms 483 | methodCallResult <- result 484 | with ex -> 485 | methodCallResult <- None 486 | | None -> () 487 | 488 | match methodCallResult with 489 | | Some ok -> return Some(JsonRpc.Response.Success(request.Id, Some ok)) 490 | | None -> return None 491 | } 492 | 493 | let handleNotification (notification: JsonRpc.Notification) = 494 | async { 495 | match 496 | notificationHandlings 497 | |> Map.tryFind notification.Method 498 | with 499 | | Some handling -> 500 | try 501 | match notification.Params with 502 | | None -> return Result.Error(JsonRpc.Error.InvalidParams()) 503 | | Some prms -> 504 | let! result = handling.Run prms 505 | return Result.Ok() 506 | with ex -> 507 | return 508 | JsonRpc.Error.InternalError(ex.ToString()) 509 | |> Result.Error 510 | | None -> return Result.Error(JsonRpc.Error.MethodNotFound()) 511 | } 512 | 513 | let messageHandler str = 514 | let messageTypeTest = JsonConvert.DeserializeObject(str, jsonSettings) 515 | 516 | match getMessageType messageTypeTest with 517 | | MessageType.Notification -> 518 | let notification = JsonConvert.DeserializeObject(str, jsonSettings) 519 | 520 | async { 521 | let! result = handleNotification notification 522 | 523 | match result with 524 | | Result.Ok _ -> () 525 | | Result.Error error -> 526 | logger.error ( 527 | Log.setMessage "HandleServerMessage - Error {error} when handling notification {notification}" 528 | >> Log.addContextDestructured "error" error 529 | >> Log.addContextDestructured "notification" notification 530 | ) 531 | //TODO: Handle error on receiving notification, send message to user? 532 | () 533 | } 534 | |> Async.StartAsTask 535 | |> ignore 536 | | MessageType.Request -> 537 | let request = JsonConvert.DeserializeObject(str, jsonSettings) 538 | 539 | async { 540 | let! result = handleRequest request 541 | 542 | match result with 543 | | Some response -> 544 | let responseString = JsonConvert.SerializeObject(response, jsonSettings) 545 | sender.Post(responseString) 546 | | None -> () 547 | } 548 | |> Async.StartAsTask 549 | |> ignore 550 | | MessageType.Response 551 | | MessageType.Error -> 552 | logger.error ( 553 | Log.setMessage "HandleServerMessage - Message had invalid jsonrpc version: {messageTypeTest}" 554 | >> Log.addContextDestructured "messageTypeTest" messageTypeTest 555 | ) 556 | 557 | () 558 | 559 | let request = JsonConvert.DeserializeObject(str, jsonSettings) 560 | 561 | async { 562 | let! result = handleRequest request 563 | 564 | match result with 565 | | Some response -> 566 | let responseString = JsonConvert.SerializeObject(response, jsonSettings) 567 | sender.Post(responseString) 568 | | None -> () 569 | } 570 | |> Async.StartAsTask 571 | |> ignore 572 | 573 | member __.SendNotification (rpcMethod: string) (requestObj: obj) = 574 | let serializedResponse = JToken.FromObject(requestObj, jsonSerializer) 575 | let notification = JsonRpc.Notification.Create(rpcMethod, Some serializedResponse) 576 | let notString = JsonConvert.SerializeObject(notification, jsonSettings) 577 | sender.Post(notString) 578 | 579 | member __.Start() = 580 | async { 581 | let si = ProcessStartInfo() 582 | si.RedirectStandardOutput <- true 583 | si.RedirectStandardInput <- true 584 | si.RedirectStandardError <- true 585 | si.UseShellExecute <- false 586 | si.WorkingDirectory <- Environment.CurrentDirectory 587 | si.FileName <- exec 588 | si.Arguments <- args 589 | 590 | let proc = 591 | try 592 | Process.Start(si) 593 | with ex -> 594 | let newEx = System.Exception(sprintf "%s on %s" ex.Message exec, ex) 595 | raise newEx 596 | 597 | inputStream <- Some(proc.StandardInput) 598 | outuptStream <- Some(proc.StandardOutput) 599 | 600 | let mutable quit = false 601 | let outStream = proc.StandardOutput.BaseStream 602 | 603 | while not quit do 604 | try 605 | let _, notificationString = LowLevel.read outStream 606 | // fprintfn stderr "[CLIENT] READING: %s" notificationString 607 | messageHandler notificationString 608 | with 609 | | :? EndOfStreamException -> quit <- true 610 | | ex -> () 611 | 612 | return () 613 | } 614 | |> Async.Start -------------------------------------------------------------------------------- /src/OptionConverter.fs: -------------------------------------------------------------------------------- 1 | namespace Ionide.LanguageServerProtocol.JsonUtils 2 | 3 | open Newtonsoft.Json 4 | open Microsoft.FSharp.Reflection 5 | open System 6 | open System.Collections.Concurrent 7 | open System.Reflection 8 | 9 | 10 | module internal Converters = 11 | open System.Collections.Concurrent 12 | 13 | let inline memorise (f: 'a -> 'b) : 'a -> 'b = 14 | let d = ConcurrentDictionary<'a, 'b>() 15 | fun key -> d.GetOrAdd(key, f) 16 | 17 | let inline memoriseByHash (f: 'a -> 'b) : 'a -> 'b = 18 | let d = ConcurrentDictionary() 19 | 20 | fun key -> 21 | let hash = key.GetHashCode() 22 | 23 | match d.TryGetValue(hash) with 24 | | (true, value) -> value 25 | | _ -> 26 | let value = f key 27 | 28 | d.TryAdd(hash, value) 29 | |> ignore 30 | 31 | value 32 | 33 | open Converters 34 | 35 | 36 | type private CaseInfo = { 37 | Info: UnionCaseInfo 38 | Fields: PropertyInfo[] 39 | GetFieldValues: obj -> obj[] 40 | Create: obj[] -> obj 41 | } 42 | 43 | type private UnionInfo = { 44 | Cases: CaseInfo[] 45 | GetTag: obj -> int 46 | } with 47 | 48 | member u.GetCaseOf(value: obj) = 49 | let tag = u.GetTag value 50 | 51 | u.Cases 52 | |> Array.find (fun case -> case.Info.Tag = tag) 53 | 54 | module private UnionInfo = 55 | 56 | let private create (ty: Type) = 57 | assert 58 | (ty 59 | |> FSharpType.IsUnion) 60 | 61 | let cases = 62 | FSharpType.GetUnionCases ty 63 | |> Array.map (fun case -> { 64 | Info = case 65 | Fields = case.GetFields() 66 | GetFieldValues = FSharpValue.PreComputeUnionReader case 67 | Create = FSharpValue.PreComputeUnionConstructor case 68 | }) 69 | 70 | { Cases = cases; GetTag = FSharpValue.PreComputeUnionTagReader ty } 71 | 72 | let get: Type -> _ = memoriseByHash (create) 73 | 74 | module Type = 75 | let numerics = [| 76 | typeof 77 | typeof 78 | typeof 79 | typeof 80 | //ENHANCEMENT: other number types 81 | |] 82 | 83 | let numericHashes = 84 | numerics 85 | |> Array.map (fun t -> t.GetHashCode()) 86 | 87 | let stringHash = typeof.GetHashCode() 88 | let boolHash = typeof.GetHashCode() 89 | 90 | let inline isOption (t: Type) = 91 | t.IsGenericType 92 | && t.GetGenericTypeDefinition() = typedefof<_ option> 93 | 94 | let inline isString (t: Type) = t.GetHashCode() = stringHash 95 | let inline isBool (t: Type) = t.GetHashCode() = boolHash 96 | 97 | let inline isNumeric (t: Type) = 98 | let hash = t.GetHashCode() 99 | 100 | numericHashes 101 | |> Array.contains hash 102 | 103 | [] 104 | type OptionConverter() = 105 | inherit JsonConverter() 106 | 107 | let getInnerType = 108 | memoriseByHash (fun (t: Type) -> 109 | let innerType = t.GetGenericArguments()[0] 110 | 111 | if innerType.IsValueType then 112 | typedefof>.MakeGenericType([| innerType |]) 113 | else 114 | innerType 115 | ) 116 | 117 | let canConvert = memoriseByHash (Type.isOption) 118 | 119 | override __.CanConvert(t) = canConvert t 120 | 121 | override __.WriteJson(writer, value, serializer) = 122 | let value = 123 | if isNull value then 124 | null 125 | else 126 | let union = UnionInfo.get (value.GetType()) 127 | let case = union.GetCaseOf value 128 | 129 | case.GetFieldValues value 130 | |> Array.head 131 | 132 | serializer.Serialize(writer, value) 133 | 134 | override __.ReadJson(reader, t, _existingValue, serializer) = 135 | match reader.TokenType with 136 | | JsonToken.Null -> null // = None 137 | | _ -> 138 | let innerType = getInnerType t 139 | 140 | let value = serializer.Deserialize(reader, innerType) 141 | 142 | if isNull value then 143 | null 144 | else 145 | let union = UnionInfo.get t 146 | union.Cases[1].Create [| value |] -------------------------------------------------------------------------------- /src/TypeDefaults.fs: -------------------------------------------------------------------------------- 1 | namespace Ionide.LanguageServerProtocol.Types 2 | 3 | [] 4 | module Extensions = 5 | 6 | type SymbolKindCapabilities = 7 | 8 | static member DefaultValueSet = [| 9 | SymbolKind.File 10 | SymbolKind.Module 11 | SymbolKind.Namespace 12 | SymbolKind.Package 13 | SymbolKind.Class 14 | SymbolKind.Method 15 | SymbolKind.Property 16 | SymbolKind.Field 17 | SymbolKind.Constructor 18 | SymbolKind.Enum 19 | SymbolKind.Interface 20 | SymbolKind.Function 21 | SymbolKind.Variable 22 | SymbolKind.Constant 23 | SymbolKind.String 24 | SymbolKind.Number 25 | SymbolKind.Boolean 26 | SymbolKind.Array 27 | |] 28 | 29 | type CompletionItemKindCapabilities = 30 | static member DefaultValueSet = [| 31 | CompletionItemKind.Text 32 | CompletionItemKind.Method 33 | CompletionItemKind.Function 34 | CompletionItemKind.Constructor 35 | CompletionItemKind.Field 36 | CompletionItemKind.Variable 37 | CompletionItemKind.Class 38 | CompletionItemKind.Interface 39 | CompletionItemKind.Module 40 | CompletionItemKind.Property 41 | CompletionItemKind.Unit 42 | CompletionItemKind.Value 43 | CompletionItemKind.Enum 44 | CompletionItemKind.Keyword 45 | CompletionItemKind.Snippet 46 | CompletionItemKind.Color 47 | CompletionItemKind.File 48 | CompletionItemKind.Reference 49 | |] 50 | 51 | type TextDocumentSyncOptions with 52 | static member Default = { 53 | OpenClose = None 54 | Change = None 55 | WillSave = None 56 | WillSaveWaitUntil = None 57 | Save = None 58 | } 59 | 60 | type WorkspaceFoldersServerCapabilities with 61 | static member Default = { Supported = None; ChangeNotifications = None } 62 | 63 | type FileOperationPatternOptions with 64 | static member Default = { IgnoreCase = None } 65 | 66 | type FileOperationOptions with 67 | static member Default = { 68 | DidCreate = None 69 | WillCreate = None 70 | DidRename = None 71 | WillRename = None 72 | DidDelete = None 73 | WillDelete = None 74 | } 75 | 76 | 77 | type WorkspaceServerCapabilities = 78 | static member Default = {| WorkspaceFolders = None; FileOperations = None |} 79 | 80 | 81 | type ServerCapabilities with 82 | static member Default = { 83 | TextDocumentSync = None 84 | HoverProvider = None 85 | CompletionProvider = None 86 | SignatureHelpProvider = None 87 | DefinitionProvider = None 88 | TypeDefinitionProvider = None 89 | ImplementationProvider = None 90 | ReferencesProvider = None 91 | DocumentHighlightProvider = None 92 | DocumentSymbolProvider = None 93 | WorkspaceSymbolProvider = None 94 | CodeActionProvider = None 95 | CodeLensProvider = None 96 | DocumentFormattingProvider = None 97 | DocumentRangeFormattingProvider = None 98 | DocumentOnTypeFormattingProvider = None 99 | RenameProvider = None 100 | DocumentLinkProvider = None 101 | ExecuteCommandProvider = None 102 | Workspace = None 103 | Experimental = None 104 | PositionEncoding = None 105 | NotebookDocumentSync = None 106 | DeclarationProvider = None 107 | ColorProvider = None 108 | FoldingRangeProvider = None 109 | SelectionRangeProvider = None 110 | CallHierarchyProvider = None 111 | LinkedEditingRangeProvider = None 112 | SemanticTokensProvider = None 113 | MonikerProvider = None 114 | TypeHierarchyProvider = None 115 | InlineValueProvider = None 116 | InlayHintProvider = None 117 | DiagnosticProvider = None 118 | } 119 | 120 | 121 | type InitializeResult with 122 | static member Default = { Capabilities = ServerCapabilities.Default; ServerInfo = None } 123 | 124 | type CompletionItem with 125 | static member Create(label: string) = { 126 | Label = label 127 | LabelDetails = None 128 | Kind = None 129 | Tags = None 130 | Detail = None 131 | Documentation = None 132 | Deprecated = None 133 | Preselect = None 134 | SortText = None 135 | FilterText = None 136 | InsertText = None 137 | InsertTextFormat = None 138 | InsertTextMode = None 139 | TextEdit = None 140 | TextEditText = None 141 | AdditionalTextEdits = None 142 | CommitCharacters = None 143 | Command = None 144 | Data = None 145 | } 146 | 147 | type WorkDoneProgressKind = 148 | | Begin 149 | | Report 150 | | End 151 | 152 | override x.ToString() = 153 | match x with 154 | | Begin -> "begin" 155 | | Report -> "report" 156 | | End -> "end" 157 | 158 | type WorkDoneProgressEnd with 159 | static member Create(?message) = { Kind = WorkDoneProgressKind.End.ToString(); Message = message } 160 | 161 | type WorkDoneProgressBegin with 162 | static member Create(title, ?cancellable, ?message, ?percentage) = { 163 | Kind = WorkDoneProgressKind.Begin.ToString() 164 | Title = title 165 | Cancellable = cancellable 166 | Message = message 167 | Percentage = percentage 168 | } 169 | 170 | type WorkDoneProgressReport with 171 | static member Create(?cancellable, ?message, ?percentage) = { 172 | Kind = WorkDoneProgressKind.Report.ToString() 173 | Cancellable = cancellable 174 | Message = message 175 | Percentage = percentage 176 | } 177 | 178 | 179 | type WorkspaceEdit with 180 | static member DocumentChangesToChanges(edits: TextDocumentEdit[]) = 181 | edits 182 | |> Array.map (fun edit -> 183 | let edits = 184 | edit.Edits 185 | |> Array.choose ( 186 | function 187 | | U2.C1 x -> Some x 188 | | _ -> None 189 | ) 190 | 191 | edit.TextDocument.Uri.ToString(), edits 192 | ) 193 | |> Map.ofArray 194 | 195 | static member CanUseDocumentChanges(capabilities: ClientCapabilities) = 196 | (capabilities.Workspace 197 | |> Option.bind (fun x -> x.WorkspaceEdit) 198 | |> Option.bind (fun x -> x.DocumentChanges)) = Some true 199 | 200 | static member Create(edits: TextDocumentEdit[], capabilities: ClientCapabilities) = 201 | if WorkspaceEdit.CanUseDocumentChanges(capabilities) then 202 | let edits = 203 | edits 204 | |> Array.map U4.C1 205 | 206 | { Changes = None; DocumentChanges = Some edits; ChangeAnnotations = None } 207 | else 208 | { 209 | Changes = Some(WorkspaceEdit.DocumentChangesToChanges edits) 210 | DocumentChanges = None 211 | ChangeAnnotations = None 212 | } 213 | 214 | type TextDocumentCodeActionResult = U2[] -------------------------------------------------------------------------------- /src/Types.fs: -------------------------------------------------------------------------------- 1 | namespace Ionide.LanguageServerProtocol.Types 2 | 3 | open Ionide.LanguageServerProtocol 4 | 5 | 6 | /// Types in typescript can have hardcoded values for their fields, this attribute is used to mark 7 | /// the default value for a field in a type and is used when deserializing the type to json 8 | /// but these types might not actually be used as a discriminated union or only partially used 9 | /// so we don't generate a dedicated union type because of that 10 | /// 11 | /// see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#resourceChanges for a dedicated example 12 | type UnionKindAttribute(value: string) = 13 | inherit System.Attribute() 14 | member x.Value = value 15 | 16 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 17 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 18 | /// serialized as a only a value based on the actual case 19 | type ErasedUnionAttribute() = 20 | inherit System.Attribute() 21 | 22 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 23 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 24 | /// serialized as a only a value based on the actual case 25 | [] 26 | type U2<'T1, 'T2> = 27 | /// Represents a single case of a Union type where the individual cases are erased when serialized or deserialized 28 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 29 | /// serialized as a only a value based on the actual case 30 | | C1 of 'T1 31 | /// Represents a single case of a Union type where the individual cases are erased when serialized or deserialized 32 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 33 | /// serialized as a only a value based on the actual case 34 | | C2 of 'T2 35 | 36 | override x.ToString() = 37 | match x with 38 | | C1 c -> string c 39 | | C2 c -> string c 40 | 41 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 42 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 43 | /// serialized as a only a value based on the actual case 44 | [] 45 | type U3<'T1, 'T2, 'T3> = 46 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 47 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 48 | /// serialized as a only a value based on the actual case 49 | | C1 of 'T1 50 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 51 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 52 | /// serialized as a only a value based on the actual case 53 | | C2 of 'T2 54 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 55 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 56 | /// serialized as a only a value based on the actual case 57 | | C3 of 'T3 58 | 59 | override x.ToString() = 60 | match x with 61 | | C1 c -> string c 62 | | C2 c -> string c 63 | | C3 c -> string c 64 | 65 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 66 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 67 | /// serialized as a only a value based on the actual case 68 | [] 69 | type U4<'T1, 'T2, 'T3, 'T4> = 70 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 71 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 72 | /// serialized as a only a value based on the actual case 73 | | C1 of 'T1 74 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 75 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 76 | /// serialized as a only a value based on the actual case 77 | | C2 of 'T2 78 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 79 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 80 | /// serialized as a only a value based on the actual case 81 | | C3 of 'T3 82 | /// Represents a Union type where the individual cases are erased when serialized or deserialized 83 | /// For instance a union could be defined as: "string | int | bool" and when serialized it would be 84 | /// serialized as a only a value based on the actual case 85 | | C4 of 'T4 86 | 87 | override x.ToString() = 88 | match x with 89 | | C1 c -> string c 90 | | C2 c -> string c 91 | | C3 c -> string 3 92 | | C4 c -> string 3 -------------------------------------------------------------------------------- /tests/Benchmarks.fs: -------------------------------------------------------------------------------- 1 | module Ionide.LanguageServerProtocol.Tests.Benchmarks 2 | 3 | open Ionide.LanguageServerProtocol.Types 4 | open Ionide.LanguageServerProtocol.JsonUtils 5 | open Ionide.LanguageServerProtocol.Server 6 | open Newtonsoft.Json.Linq 7 | open BenchmarkDotNet.Attributes 8 | open BenchmarkDotNet.Running 9 | open BenchmarkDotNet.Configs 10 | open System 11 | open System.Collections.Concurrent 12 | open System.Collections.Generic 13 | open BenchmarkDotNet.Order 14 | 15 | let inline private memorise (f: 'a -> 'b) : 'a -> 'b = 16 | let d = ConcurrentDictionary<'a, 'b>() 17 | fun key -> d.GetOrAdd(key, f) 18 | 19 | let inline private memoriseByHash (f: 'a -> 'b) : 'a -> 'b = 20 | let d = ConcurrentDictionary() 21 | 22 | fun key -> 23 | let hash = key.GetHashCode() 24 | 25 | match d.TryGetValue(hash) with 26 | | (true, value) -> value 27 | | _ -> 28 | let value = f key 29 | 30 | d.TryAdd(hash, value) 31 | |> ignore 32 | 33 | value 34 | 35 | [] 36 | [] 37 | [] 38 | type TypeCheckBenchmarks() = 39 | static let values: obj array = [| 40 | 1 41 | 3.14 42 | true 43 | "foo" 44 | 4uy 45 | Some "foo" 46 | 123456 47 | "bar" 48 | false 49 | Some true 50 | 987.654321 51 | 0 52 | Some 0 53 | 5u 54 | Some "string" 55 | Some "bar" 56 | Some "baz" 57 | Some "lorem ipsum dolor sit" 58 | 321654 59 | 2.71828 60 | "lorem ipsum dolor sit" 61 | Some 42 62 | Some true 63 | Some 3.14 64 | |] 65 | 66 | static let types = 67 | values 68 | |> Array.map (fun v -> v.GetType()) 69 | 70 | static let isOptionType (ty: Type) = 71 | ty.IsGenericType 72 | && ty.GetGenericTypeDefinition() = typedefof<_ option> 73 | 74 | static let memorisedIsOptionType = memorise isOptionType 75 | static let memorisedHashIsOptionType = memoriseByHash isOptionType 76 | 77 | [] 78 | member _.IsNumeric_typeof() = 79 | let mutable count = 0 80 | 81 | for ty in types do 82 | if 83 | Type.numerics 84 | |> Array.exists ((=) ty) 85 | then 86 | count <- count + 1 87 | 88 | count 89 | 90 | [] 91 | member _.IsNumeric_hash() = 92 | let mutable count = 0 93 | 94 | for ty in types do 95 | let hash = ty.GetHashCode() 96 | 97 | if 98 | Type.numericHashes 99 | |> Array.contains hash 100 | then 101 | count <- count + 1 102 | 103 | count 104 | 105 | [] 106 | member _.IsBool_typeof() = 107 | let mutable count = 0 108 | 109 | for ty in types do 110 | if ty = typeof then 111 | count <- count + 1 112 | 113 | count 114 | 115 | [] 116 | member _.IsBool_hash() = 117 | let mutable count = 0 118 | 119 | for ty in types do 120 | if ty.GetHashCode() = Type.boolHash then 121 | count <- count + 1 122 | 123 | count 124 | 125 | [] 126 | member _.IsString_typeof() = 127 | let mutable count = 0 128 | 129 | for ty in types do 130 | if ty = typeof then 131 | count <- count + 1 132 | 133 | count 134 | 135 | [] 136 | member _.IsString_hash() = 137 | let mutable count = 0 138 | 139 | for ty in types do 140 | if ty.GetHashCode() = Type.stringHash then 141 | count <- count + 1 142 | 143 | count 144 | 145 | [] 146 | member _.IsOption_check() = 147 | let mutable count = 0 148 | 149 | for ty in types do 150 | if isOptionType ty then 151 | count <- count + 1 152 | 153 | count 154 | 155 | [] 156 | member _.IsOption_memoriseType() = 157 | let mutable count = 0 158 | 159 | for ty in types do 160 | if memorisedIsOptionType ty then 161 | count <- count + 1 162 | 163 | count 164 | 165 | [] 166 | member _.IsOption_memoriseHash() = 167 | let mutable count = 0 168 | 169 | for ty in types do 170 | if memorisedHashIsOptionType ty then 171 | count <- count + 1 172 | 173 | count 174 | 175 | module Example = 176 | open System.Text.Json.Serialization 177 | 178 | type private Random with 179 | 180 | member rand.NextBool() = 181 | // 2nd is exclusive! 182 | rand.Next(0, 2) = 0 183 | 184 | member rand.NextOption(value) = if rand.NextBool() then Some value else None 185 | member rand.NextCount depth = rand.Next(2, max 4 depth) 186 | 187 | member rand.NextDepth depth = 188 | if depth <= 1 then 189 | 0 190 | elif depth = 2 then 191 | 1 192 | else 193 | let lower = max 2 (depth - 1) 194 | let upper = max lower (depth - 1) 195 | rand.Next(lower, upper + 1) 196 | 197 | [] 198 | type SingleCaseUnion = 199 | | Lorem 200 | | Ipsum 201 | 202 | type SingleCaseUnionHolder = { SingleCaseUnion: SingleCaseUnion } 203 | 204 | module SingleCaseUnionHolder = 205 | let gen (rand: Random) (depth: int) = { 206 | SingleCaseUnion = 207 | if rand.NextBool() then 208 | SingleCaseUnion.Ipsum 209 | else 210 | SingleCaseUnion.Ipsum 211 | } 212 | 213 | type WithExtensionData = { 214 | NoExtensionData: string 215 | [] 216 | mutable AdditionalData: IDictionary 217 | } 218 | 219 | module WithExtensionData = 220 | let gen (rand: Random) (depth: int) = { 221 | NoExtensionData = $"WithExtensionData {depth}" 222 | AdditionalData = 223 | List.init 224 | (rand.NextCount depth) 225 | (fun i -> 226 | let key = $"Data{depth}Ele{i}" 227 | let value = JToken.FromObject(i * depth) 228 | (key, value) 229 | ) 230 | |> Map.ofList 231 | } 232 | 233 | type RecordWithOption = { 234 | RequiredValue: string 235 | OptionalValue: string option 236 | AnotherOptionalValue: int option 237 | FinalOptionalValue: int option 238 | } 239 | 240 | module RecordWithOption = 241 | let gen (rand: Random) (depth: int) = { 242 | RequiredValue = $"RecordWithOption {depth}" 243 | OptionalValue = rand.NextOption $"Hello {depth}" 244 | AnotherOptionalValue = 245 | rand.NextOption( 246 | 42000 247 | + depth 248 | ) 249 | FinalOptionalValue = 250 | rand.NextOption( 251 | 13000 252 | + depth 253 | ) 254 | } 255 | 256 | [] 257 | [] 258 | type ErasedUnionData = 259 | | Alpha of string 260 | | Beta of int 261 | | Gamma of bool 262 | | Delta of float 263 | | Epsilon of RecordWithOption 264 | 265 | module ErasedUnionData = 266 | let gen (rand: Random) (depth: int) = 267 | match rand.Next(0, 5) with 268 | | 0 -> ErasedUnionData.Alpha $"Erased {depth}" 269 | | 1 -> 270 | ErasedUnionData.Beta( 271 | 42000 272 | + depth 273 | ) 274 | | 2 -> ErasedUnionData.Gamma false 275 | | 3 -> 276 | ErasedUnionData.Delta( 277 | 42000.123 278 | + (float depth) 279 | ) 280 | | 4 -> ErasedUnionData.Epsilon(RecordWithOption.gen rand (depth - 1)) 281 | | _ -> failwith "unreachable" 282 | 283 | type ErasedUnionDataHolder = { ErasedUnion: ErasedUnionData } 284 | 285 | module ErasedUnionDataHolder = 286 | let gen (rand: Random) (depth: int) = { ErasedUnion = ErasedUnionData.gen rand depth } 287 | 288 | type U2Holder = { 289 | BoolString: U2 290 | StringInt: U2 291 | BoolErasedUnionData: U2 292 | } 293 | 294 | module U2Holder = 295 | let gen (rand: Random) (depth: int) = { 296 | BoolString = if rand.NextBool() then U2.C1 true else U2.C2 $"U2 {depth}" 297 | StringInt = 298 | if rand.NextBool() then 299 | U2.C1 $"U2 {depth}" 300 | else 301 | U2.C2( 302 | 42000 303 | + depth 304 | ) 305 | BoolErasedUnionData = 306 | if rand.NextBool() then 307 | U2.C1 true 308 | else 309 | U2.C2(ErasedUnionDataHolder.gen rand (depth - 1)) 310 | } 311 | 312 | [] 313 | type MyEnum = 314 | | X = 1 315 | | Y = 2 316 | | Z = 3 317 | 318 | type MyEnumHolder = { EnumValue: MyEnum; EnumArray: MyEnum[] } 319 | 320 | module MyEnumHolder = 321 | let gen (rand: Random) (depth: int) = 322 | let n = Enum.GetNames(typeof).Length 323 | 324 | { 325 | EnumValue = 326 | rand.Next(0, n) 327 | |> enum<_> 328 | EnumArray = 329 | Array.init 330 | (rand.NextCount depth) 331 | (fun i -> 332 | rand.Next(0, n) 333 | |> enum<_> 334 | ) 335 | } 336 | 337 | type MapHolder = { MyMap: Map } 338 | 339 | module MapHolder = 340 | let gen (rand: Random) (depth: int) = { 341 | MyMap = 342 | Array.init 343 | (rand.NextCount depth) 344 | (fun i -> 345 | let key = $"Key{i}" 346 | let value = $"Data{i}@{depth}" 347 | (key, value) 348 | ) 349 | |> Map.ofArray 350 | } 351 | 352 | type BasicData = { 353 | IntData: int 354 | FloatData: float 355 | BoolData: bool 356 | StringData: string 357 | CharData: char 358 | StringOptionData: string option 359 | IntArrayOptionData: int[] option 360 | } 361 | 362 | module BasicData = 363 | let gen (rand: Random) (depth: int) = { 364 | IntData = rand.Next(0, 500) 365 | FloatData = rand.NextDouble() 366 | BoolData = rand.NextBool() 367 | StringData = $"Data {depth}" 368 | CharData = '_' 369 | StringOptionData = rand.NextOption $"Option {depth}" 370 | IntArrayOptionData = 371 | Array.init (rand.NextCount depth) id 372 | |> rand.NextOption 373 | } 374 | 375 | 376 | [] 377 | [] 378 | type Data = 379 | | SingleCaseUnion of SingleCaseUnionHolder 380 | | WithExtensionData of WithExtensionData 381 | | RecordWithOption of RecordWithOption 382 | | ErasedUnion of ErasedUnionDataHolder 383 | | U2 of U2Holder 384 | | Enum of MyEnumHolder 385 | | Map of MapHolder 386 | | BasicData of BasicData 387 | | More of Data[] 388 | 389 | module Data = 390 | let rec gen (rand: Random) (depth: int) = 391 | match rand.Next(0, 11) with 392 | | _ when depth <= 0 -> Data.More [||] 393 | | 0 -> Data.SingleCaseUnion(SingleCaseUnionHolder.gen rand depth) 394 | | 1 -> Data.WithExtensionData(WithExtensionData.gen rand depth) 395 | | 2 -> Data.RecordWithOption(RecordWithOption.gen rand depth) 396 | | 3 -> Data.ErasedUnion(ErasedUnionDataHolder.gen rand depth) 397 | | 4 -> Data.U2(U2Holder.gen rand depth) 398 | | 5 -> Data.Enum(MyEnumHolder.gen rand depth) 399 | | 6 -> Data.Map(MapHolder.gen rand depth) 400 | | 7 -> Data.BasicData(BasicData.gen rand depth) 401 | | 8 402 | | 9 403 | | 10 -> 404 | Data.More( 405 | Array.init 406 | (rand.NextCount depth) 407 | (fun _ -> 408 | let depth = rand.NextDepth depth 409 | gen rand depth 410 | ) 411 | ) 412 | | _ -> failwith "unreachable" 413 | 414 | let createData (seed: int, additionalWidth: int, maxDepth: int) = 415 | // Note: deterministic (-> seed) 416 | let rand = Random(seed) 417 | 418 | let always = [| 419 | Data.SingleCaseUnion(SingleCaseUnionHolder.gen rand maxDepth) 420 | Data.WithExtensionData(WithExtensionData.gen rand maxDepth) 421 | Data.RecordWithOption(RecordWithOption.gen rand maxDepth) 422 | Data.ErasedUnion(ErasedUnionDataHolder.gen rand maxDepth) 423 | Data.U2(U2Holder.gen rand maxDepth) 424 | Data.Enum(MyEnumHolder.gen rand maxDepth) 425 | Data.Map(MapHolder.gen rand maxDepth) 426 | Data.BasicData(BasicData.gen rand maxDepth) 427 | |] 428 | 429 | let additional = 430 | Array.init 431 | additionalWidth 432 | (fun _ -> 433 | let depth = rand.NextDepth maxDepth 434 | Data.gen rand depth 435 | ) 436 | 437 | let data = Array.append always additional 438 | Data.More data 439 | 440 | [] 441 | [] 442 | [] 443 | [] 444 | type MultipleTypesBenchmarks() = 445 | let initializeParams: InitializeParams = { 446 | ProcessId = Some 42 447 | ClientInfo = Some { Name = "foo"; Version = None } 448 | Locale = None 449 | RootPath = Some "/" 450 | RootUri = Some "file://..." 451 | InitializationOptions = None 452 | WorkDoneToken = None 453 | Trace = Some TraceValues.Off 454 | Capabilities = 455 | 456 | { 457 | Workspace = 458 | Some { 459 | ApplyEdit = Some true 460 | WorkspaceEdit = 461 | Some { 462 | DocumentChanges = Some true 463 | ResourceOperations = 464 | Some [| 465 | ResourceOperationKind.Create 466 | ResourceOperationKind.Rename 467 | ResourceOperationKind.Delete 468 | |] 469 | FailureHandling = Some FailureHandlingKind.Abort 470 | NormalizesLineEndings = None 471 | ChangeAnnotationSupport = Some { GroupsOnLabel = Some false } 472 | } 473 | DidChangeConfiguration = None 474 | DidChangeWatchedFiles = None 475 | Symbol = 476 | Some { 477 | DynamicRegistration = Some false 478 | SymbolKind = Some { ValueSet = Some SymbolKindCapabilities.DefaultValueSet } 479 | TagSupport = None 480 | ResolveSupport = None 481 | } 482 | SemanticTokens = Some { RefreshSupport = Some true } 483 | InlayHint = Some { RefreshSupport = Some false } 484 | InlineValue = Some { RefreshSupport = Some false } 485 | CodeLens = Some { RefreshSupport = Some true } 486 | ExecuteCommand = None 487 | WorkspaceFolders = None 488 | Configuration = None 489 | FileOperations = None 490 | Diagnostics = None 491 | 492 | } 493 | NotebookDocument = None 494 | TextDocument = 495 | Some { 496 | Synchronization = 497 | Some { 498 | DynamicRegistration = Some true 499 | WillSave = Some true 500 | WillSaveWaitUntil = Some false 501 | DidSave = Some true 502 | } 503 | PublishDiagnostics = 504 | Some { 505 | RelatedInformation = None 506 | TagSupport = None 507 | VersionSupport = None 508 | CodeDescriptionSupport = None 509 | DataSupport = None 510 | } 511 | Completion = None 512 | Hover = 513 | Some { 514 | DynamicRegistration = Some true 515 | ContentFormat = 516 | Some [| 517 | MarkupKind.PlainText 518 | MarkupKind.Markdown 519 | |] 520 | } 521 | SignatureHelp = 522 | Some { 523 | DynamicRegistration = Some true 524 | SignatureInformation = 525 | Some { 526 | DocumentationFormat = None 527 | ParameterInformation = None 528 | ActiveParameterSupport = None 529 | } 530 | ContextSupport = None 531 | } 532 | Declaration = Some { DynamicRegistration = Some false; LinkSupport = Some false } 533 | References = Some { DynamicRegistration = Some false } 534 | DocumentHighlight = Some { DynamicRegistration = None } 535 | DocumentSymbol = None 536 | Formatting = Some { DynamicRegistration = Some true } 537 | RangeFormatting = Some { DynamicRegistration = Some true } 538 | OnTypeFormatting = None 539 | Definition = Some { DynamicRegistration = Some false; LinkSupport = Some false } 540 | TypeDefinition = Some { DynamicRegistration = Some false; LinkSupport = Some false } 541 | Implementation = Some { DynamicRegistration = Some false; LinkSupport = Some false } 542 | CodeAction = 543 | Some { 544 | DynamicRegistration = Some true 545 | CodeActionLiteralSupport = 546 | Some { 547 | CodeActionKind = { 548 | ValueSet = [| 549 | "foo" 550 | "bar" 551 | "baz" 552 | "alpha" 553 | "beta" 554 | "gamma" 555 | "delta" 556 | "x" 557 | "y" 558 | "z" 559 | |] 560 | } 561 | } 562 | IsPreferredSupport = Some true 563 | DisabledSupport = Some false 564 | DataSupport = None 565 | ResolveSupport = 566 | Some { 567 | Properties = [| 568 | "foo" 569 | "bar" 570 | "baz" 571 | |] 572 | } 573 | HonorsChangeAnnotations = Some false 574 | } 575 | CodeLens = Some { DynamicRegistration = Some true } 576 | DocumentLink = Some { DynamicRegistration = Some true; TooltipSupport = None } 577 | ColorProvider = Some { DynamicRegistration = Some true } 578 | Rename = None 579 | FoldingRange = 580 | Some { 581 | DynamicRegistration = Some false 582 | LineFoldingOnly = Some true 583 | RangeLimit = None 584 | FoldingRangeKind = None 585 | FoldingRange = None 586 | } 587 | SelectionRange = Some { DynamicRegistration = None } 588 | LinkedEditingRange = Some { DynamicRegistration = None } 589 | CallHierarchy = Some { DynamicRegistration = None } 590 | SemanticTokens = 591 | Some { 592 | DynamicRegistration = Some false 593 | Requests = { Range = Some(U2.C1 true); Full = Some(U2.C2 { Delta = Some true }) } 594 | TokenTypes = 595 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Proin tortor purus platea sit eu id nisi litora libero. Neque vulputate consequat ac amet augue blandit maximus aliquet congue. Pharetra vestibulum posuere ornare faucibus fusce dictumst orci aenean eu facilisis ut volutpat commodo senectus purus himenaeos fames primis convallis nisi." 596 | |> fun s -> s.Split(' ') 597 | TokenModifiers = 598 | "Phasellus fermentum malesuada phasellus netus dictum aenean placerat egestas amet. Ornare taciti semper dolor tristique morbi. Sem leo tincidunt aliquet semper eu lectus scelerisque quis. Sagittis vivamus mollis nisi mollis enim fermentum laoreet." 599 | |> fun s -> s.Split(' ') 600 | Formats = [| TokenFormat.Relative |] 601 | OverlappingTokenSupport = Some false 602 | MultilineTokenSupport = Some true 603 | ServerCancelSupport = None 604 | AugmentsSyntaxTokens = None 605 | } 606 | TypeHierarchy = Some { DynamicRegistration = None } 607 | Moniker = Some { DynamicRegistration = None } 608 | InlineValue = Some { DynamicRegistration = None } 609 | InlayHint = 610 | Some { 611 | DynamicRegistration = Some true 612 | ResolveSupport = 613 | Some { 614 | Properties = [| 615 | "Tooltip" 616 | "Position" 617 | "TextEdits" 618 | |] 619 | } 620 | } 621 | Diagnostic = Some { DynamicRegistration = None; RelatedDocumentSupport = None } 622 | } 623 | General = None 624 | Experimental = None 625 | Window = None 626 | } 627 | WorkspaceFolders = 628 | Some [| 629 | { Uri = "..."; Name = "foo" } 630 | { Uri = "/"; Name = "bar" } 631 | { 632 | Uri = "something long stuff and even longer and longer and longer" 633 | Name = "bar" 634 | } 635 | |] 636 | } 637 | 638 | let inlayHint: InlayHint = { 639 | Label = 640 | U2.C2 [| 641 | { 642 | InlayHintLabelPart.Value = "1st label" 643 | Tooltip = Some(U2.C1 "1st label tooltip") 644 | Location = Some { Uri = "1st"; Range = mkRange' (1u, 2u) (3u, 4u) } 645 | Command = None 646 | } 647 | { 648 | Value = "2nd label" 649 | Tooltip = Some(U2.C1 "1st label tooltip") 650 | Location = Some { Uri = "2nd"; Range = mkRange' (5u, 8u) (10u, 9u) } 651 | Command = Some { Title = "2nd command"; Command = "foo"; Arguments = None } 652 | } 653 | { 654 | InlayHintLabelPart.Value = "3rd label" 655 | Tooltip = 656 | Some( 657 | U2.C2 { 658 | Kind = MarkupKind.Markdown 659 | Value = 660 | """ 661 | # Header 662 | Description 663 | * List 1 664 | * List 2 665 | """ 666 | } 667 | ) 668 | Location = Some { Uri = "3rd"; Range = mkRange' (1u, 2u) (3u, 4u) } 669 | Command = None 670 | } 671 | |] 672 | Position = { Line = 5u; Character = 10u } 673 | Kind = Some InlayHintKind.Type 674 | TextEdits = 675 | Some [| 676 | { Range = mkRange' (5u, 10u) (6u, 5u); NewText = "foo bar" } 677 | { Range = mkRange' (5u, 0u) (5u, 2u); NewText = "baz" } 678 | |] 679 | Tooltip = Some(U2.C2 { Kind = MarkupKind.PlainText; Value = "some tooltip" }) 680 | PaddingLeft = Some true 681 | PaddingRight = Some false 682 | Data = Some(JToken.FromObject "some data") 683 | } 684 | 685 | let allLsp: obj[] = [| 686 | initializeParams 687 | inlayHint 688 | |] 689 | 690 | /// Some complex data which covers all converters 691 | let example = Example.createData (1234, 9, 5) 692 | let option = {| Some = Some 123; None = (None: int option) |} 693 | 694 | let withCounts (counts) data = 695 | data 696 | |> Array.collect (fun data -> 697 | counts 698 | |> Array.map (fun count -> [| 699 | box count 700 | box data 701 | |]) 702 | ) 703 | 704 | member _.AllLsp_Roundtrip() = 705 | for o in allLsp do 706 | let json = 707 | inlayHint 708 | |> serialize 709 | 710 | let res = json.ToObject(o.GetType(), jsonRpcFormatter.JsonSerializer) 711 | () 712 | 713 | [] 714 | [] 715 | [] 716 | member b.AllLsp_Roundtrips(count: int) = 717 | for _ in 1..count do 718 | b.AllLsp_Roundtrip() 719 | 720 | member _.Example_Roundtrip() = 721 | let json = 722 | example 723 | |> serialize 724 | 725 | let res = json.ToObject(example.GetType(), jsonRpcFormatter.JsonSerializer) 726 | () 727 | 728 | [] 729 | [] 730 | [] 731 | member b.Example_Roundtrips(count: int) = 732 | for _ in 1..count do 733 | b.Example_Roundtrip() 734 | 735 | [] 736 | [] 737 | [] 738 | member _.Option_Roundtrips(count: int) = 739 | for _ in 1..count do 740 | let json = 741 | option 742 | |> serialize 743 | 744 | let _ = json.ToObject(option.GetType(), jsonRpcFormatter.JsonSerializer) 745 | () 746 | 747 | member _.SingleCaseUnion_ArgumentsSource() = 748 | [| 749 | Example.SingleCaseUnion.Ipsum 750 | Example.SingleCaseUnion.Lorem 751 | |] 752 | |> withCounts [| 753 | 1 754 | 50 755 | |] 756 | 757 | [] 758 | [] 759 | member _.SingleCaseUnion_Roundtrips(count: int, data: Example.SingleCaseUnion) = 760 | for _ in 1..count do 761 | let json = 762 | data 763 | |> serialize 764 | 765 | let _ = json.ToObject(typeof, jsonRpcFormatter.JsonSerializer) 766 | () 767 | 768 | member _.ErasedUnion_ArgumentsSource() = 769 | [| 770 | Example.ErasedUnionData.Alpha "foo" 771 | Example.ErasedUnionData.Beta 42 772 | Example.ErasedUnionData.Gamma true 773 | Example.ErasedUnionData.Delta 3.14 774 | Example.ErasedUnionData.Epsilon { 775 | RequiredValue = "foo" 776 | OptionalValue = None 777 | AnotherOptionalValue = None 778 | FinalOptionalValue = None 779 | } 780 | |] 781 | |> withCounts [| 782 | 1 783 | 50 784 | |] 785 | 786 | [] 787 | [] 788 | member _.ErasedUnion_Roundtrips(count: int, data: Example.ErasedUnionData) = 789 | for _ in 1..count do 790 | let json = 791 | data 792 | |> serialize 793 | 794 | let _ = json.ToObject(typeof, jsonRpcFormatter.JsonSerializer) 795 | () 796 | 797 | 798 | let run (args: string[]) = 799 | let switcher = 800 | BenchmarkSwitcher.FromTypes( 801 | [| 802 | typeof 803 | typeof 804 | |] 805 | ) 806 | 807 | switcher.Run(args) 808 | |> ignore 809 | 810 | 0 -------------------------------------------------------------------------------- /tests/Ionide.LanguageServerProtocol.Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Ionide.LanguageServerProtocol.Tests.Root 2 | 3 | open Expecto.Tests 4 | 5 | [] 6 | let main args = 7 | let (|ShouldRunBenchmarks|_|) (args: string[]) = 8 | let nArgs = args.Length 9 | 10 | let markers = [| 11 | "--benchmark" 12 | "--benchmarks" 13 | "-b" 14 | |] 15 | 16 | let args = 17 | args 18 | |> Array.filter (fun arg -> 19 | markers 20 | |> Array.contains arg 21 | |> not 22 | ) 23 | 24 | if args.Length = nArgs then None else Some args 25 | 26 | match args with 27 | | ShouldRunBenchmarks args -> 28 | // `--filter *` to run all 29 | Benchmarks.run args 30 | | _ -> Expecto.Tests.runTestsWithCLIArgs [ Sequenced ] args Tests.tests -------------------------------------------------------------------------------- /tests/Shotgun.fs: -------------------------------------------------------------------------------- 1 | /// Checks all LSP types with FsCheck: `(value |> serialize |> deserialize) = value` 2 | /// 3 | /// Note: success doesn't necessary mean everything got correctly (de)serialized, 4 | /// just everything can do a roundtrip (`(value |> serialize |> deserialize) = value`). 5 | /// Serialized JSON might still be incorrect! 6 | /// -> Just checking for exceptions while (de)serializing! 7 | module Ionide.LanguageServerProtocol.Tests.Shotgun 8 | 9 | open System 10 | open System.Reflection 11 | open Ionide.LanguageServerProtocol.Types 12 | open FsCheck 13 | open Newtonsoft.Json.Linq 14 | open Expecto 15 | open Ionide.LanguageServerProtocol.Server 16 | open Ionide.LanguageServerProtocol.Types 17 | 18 | // must be public 19 | type Gens = 20 | static member NoNulls<'T>() : Arbitrary<'T> = 21 | if typeof<'T>.IsValueType then 22 | Arb.Default.Derive<'T>() 23 | else 24 | Arb.Default.Derive<'T>() 25 | |> Arb.filter (fun v -> 26 | (box v) 27 | <> null 28 | ) 29 | 30 | static member JToken() : Arbitrary = 31 | // actual value doesn't matter -> handled by user 32 | // and complexer JTokens cannot be directly compared with `=` 33 | JToken.FromObject(123) 34 | |> Gen.constant 35 | |> Arb.fromGen 36 | 37 | static member String() : Arbitrary = 38 | Arb.Default.String() 39 | |> Arb.filter (fun s -> not (isNull s)) 40 | 41 | static member Float() : Arbitrary = 42 | Arb.Default.NormalFloat() 43 | |> Arb.convert float NormalFloat 44 | 45 | static member Uri() : Arbitrary = 46 | // actual value doesn't matter -> always use example uri 47 | System.Uri("foo://example.com:8042/over/there?name=ferret#nose") 48 | |> Gen.constant 49 | |> Arb.fromGen 50 | 51 | static member CreateFile() : Arbitrary = 52 | let create annotationId uri options : CreateFile = { 53 | AnnotationId = annotationId 54 | Uri = uri 55 | Options = options 56 | Kind = "create" 57 | } 58 | 59 | Gen.map3 create Arb.generate Arb.generate Arb.generate 60 | |> Arb.fromGen 61 | 62 | static member RenameFile() : Arbitrary = 63 | let create annotationId oldUri newUri options : RenameFile = { 64 | AnnotationId = annotationId 65 | OldUri = oldUri 66 | NewUri = newUri 67 | Options = options 68 | Kind = "rename" 69 | } 70 | 71 | Gen.map4 create Arb.generate Arb.generate Arb.generate Arb.generate 72 | |> Arb.fromGen 73 | 74 | static member DeleteFile() : Arbitrary = 75 | let create annotationId uri options : DeleteFile = { 76 | AnnotationId = annotationId 77 | Uri = uri 78 | Options = options 79 | Kind = "delete" 80 | } 81 | 82 | Gen.map3 create Arb.generate Arb.generate Arb.generate 83 | |> Arb.fromGen 84 | 85 | static member DocumentSymbol() : Arbitrary = 86 | // DocumentSymbol is recursive -> Stack overflow when default generation 87 | // https://fscheck.github.io/FsCheck/TestData.html#Generating-recursive-data-types 88 | let maxDepth = 5 89 | 90 | let create name detail kind range selectionRange children = { 91 | Name = name 92 | Detail = detail 93 | Kind = kind 94 | Tags = None 95 | Deprecated = None 96 | Range = range 97 | SelectionRange = selectionRange 98 | Children = children 99 | } 100 | 101 | let genDocSymbol = Gen.map6 create Arb.generate Arb.generate Arb.generate Arb.generate Arb.generate 102 | // Children is still open 103 | let rec gen size = 104 | let size = min size maxDepth 105 | 106 | if size <= 0 then 107 | genDocSymbol ( 108 | Gen.oneof [ 109 | Gen.constant (None) 110 | Gen.constant (Some [||]) 111 | ] 112 | ) 113 | else 114 | let children = 115 | gen (size - 1) 116 | |> Gen.arrayOf 117 | |> Gen.optionOf 118 | 119 | genDocSymbol children 120 | 121 | Gen.sized gen 122 | |> Arb.fromGen 123 | 124 | static member SelectionRange() : Arbitrary = 125 | let maxDepth = 5 126 | let create range parent = { Range = range; Parent = parent } 127 | let genSelectionRange = Gen.map2 create Arb.generate 128 | 129 | let rec gen size = 130 | let size = min size maxDepth 131 | 132 | if size <= 0 then 133 | genSelectionRange (Gen.constant None) 134 | else 135 | let parent = 136 | gen (size - 1) 137 | |> Gen.optionOf 138 | 139 | genSelectionRange parent 140 | 141 | Gen.sized gen 142 | |> Arb.fromGen 143 | 144 | let private fsCheckConfig = { FsCheckConfig.defaultConfig with arbitrary = [ typeof ] } 145 | 146 | type private Roundtripper = 147 | static member ThereAndBackAgain(input: 'a) = 148 | input 149 | |> serialize 150 | |> deserialize<'a> 151 | 152 | static member TestThereAndBackAgain(input: 'a) = 153 | let output = Roundtripper.ThereAndBackAgain input 154 | // Fails: Dictionary doesn't support structural identity (-> different instances with same content aren't equal) 155 | // Expect.equal output input "Input -> serialize -> deserialize should be Input again" 156 | Utils.convertExtensionDataDictionariesToMap output 157 | Utils.convertExtensionDataDictionariesToMap input 158 | Expect.equal output input "Input -> serialize -> deserialize should be Input again" 159 | 160 | static member TestProperty<'a when 'a: equality> name = 161 | testPropertyWithConfig fsCheckConfig name (Roundtripper.TestThereAndBackAgain<'a>) 162 | 163 | let tests = 164 | testList "shotgun" [ 165 | // Type Abbreviations get erased 166 | // -> not available as type and don't get pick up below 167 | // -> specify manual 168 | let abbrevTys = [| 169 | nameof DocumentUri, typeof 170 | nameof DocumentSelector, typeof 171 | nameof TextDocumentCodeActionResult, typeof 172 | 173 | |] 174 | 175 | let tys = 176 | let shouldTestType (t: Type) = 177 | Utils.isLspType 178 | [ 179 | not 180 | << Lsp.Is.Generic 181 | not 182 | << Lsp.Is.Nested 183 | ] 184 | t 185 | && t.Name = "ApplyWorkspaceEditParams" 186 | 187 | 188 | let example = typeof 189 | let ass = example.Assembly 190 | 191 | ass.GetTypes() 192 | |> Array.filter shouldTestType 193 | |> Array.map (fun t -> t.Name, t) 194 | |> Array.append abbrevTys 195 | |> Array.sortBy fst 196 | 197 | let propTester = 198 | typeof 199 | .GetMethod( 200 | nameof (Roundtripper.TestProperty), 201 | BindingFlags.Static 202 | ||| BindingFlags.NonPublic 203 | ) 204 | 205 | for (name, ty) in tys do 206 | let m = propTester.MakeGenericMethod([| ty |]) 207 | 208 | m.Invoke(null, [| name |]) 209 | |> unbox 210 | ] -------------------------------------------------------------------------------- /tests/StartWithSetup.fs: -------------------------------------------------------------------------------- 1 | module Ionide.LanguageServerProtocol.Tests.StartWithSetup 2 | 3 | open Expecto 4 | open System.IO.Pipes 5 | open System.IO 6 | open Ionide.LanguageServerProtocol 7 | open Ionide.LanguageServerProtocol.Server 8 | open Nerdbank.Streams 9 | open StreamJsonRpc 10 | open System 11 | open System.Reflection 12 | open StreamJsonRpc.Protocol 13 | 14 | type TestLspClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) = 15 | inherit LspClient() 16 | 17 | let setupEndpoints (_: LspClient) : Map = 18 | [] 19 | |> Map.ofList 20 | 21 | let requestWithContentLength (request: string) = $"Content-Length: {request.Length}\r\n\r\n{request}" 22 | 23 | let shutdownRequest = @"{""jsonrpc"":""2.0"",""method"":""shutdown"",""id"":1}" 24 | 25 | let exitRequest = @"{""jsonrpc"":""2.0"",""method"":""exit"",""id"":1}" 26 | 27 | 28 | type Foo = { bar: string; baz: string } 29 | 30 | let tests = 31 | testList "startWithSetup" [ 32 | testAsync "can start up multiple times in same process" { 33 | use inputServerPipe1 = new AnonymousPipeServerStream() 34 | use inputClientPipe1 = new AnonymousPipeClientStream(inputServerPipe1.GetClientHandleAsString()) 35 | use outputServerPipe1 = new AnonymousPipeServerStream() 36 | 37 | use inputWriter1 = new StreamWriter(inputServerPipe1) 38 | inputWriter1.AutoFlush <- true 39 | 40 | let server1 = 41 | async { 42 | let result = 43 | startWithSetup setupEndpoints inputClientPipe1 outputServerPipe1 (fun x -> new TestLspClient(x)) defaultRpc 44 | 45 | Expect.equal (int result) 0 "server startup failed" 46 | } 47 | 48 | let! server1Async = Async.StartChild(server1) 49 | 50 | use inputServerPipe2 = new AnonymousPipeServerStream() 51 | use inputClientPipe2 = new AnonymousPipeClientStream(inputServerPipe2.GetClientHandleAsString()) 52 | use outputServerPipe2 = new AnonymousPipeServerStream() 53 | 54 | use inputWriter2 = new StreamWriter(inputServerPipe2) 55 | inputWriter2.AutoFlush <- true 56 | 57 | let server2 = 58 | async { 59 | let result = 60 | (startWithSetup setupEndpoints inputClientPipe2 outputServerPipe2 (fun x -> new TestLspClient(x)) defaultRpc) 61 | 62 | Expect.equal (int result) 0 "server startup failed" 63 | } 64 | 65 | let! server2Async = Async.StartChild(server2) 66 | 67 | inputWriter1.Write(requestWithContentLength (shutdownRequest)) 68 | inputWriter1.Write(requestWithContentLength (exitRequest)) 69 | 70 | inputWriter2.Write(requestWithContentLength (shutdownRequest)) 71 | inputWriter2.Write(requestWithContentLength (exitRequest)) 72 | 73 | do! server1Async 74 | do! server2Async 75 | } 76 | 77 | testCaseAsync "Handle JsonSerializationError gracefully" 78 | <| async { 79 | 80 | let struct (server, client) = FullDuplexStream.CreatePair() 81 | 82 | use jsonRpcHandler = new HeaderDelimitedMessageHandler(server, defaultJsonRpcFormatter ()) 83 | use serverRpc = defaultRpc jsonRpcHandler 84 | 85 | let functions: Map = Map [ "lol", Func(fun (foo: Foo) -> foo) :> Delegate ] 86 | 87 | functions 88 | |> Seq.iter (fun (KeyValue(name, rpcDelegate)) -> 89 | let rpcAttribute = JsonRpcMethodAttribute(name) 90 | rpcAttribute.UseSingleObjectParameterDeserialization <- true 91 | serverRpc.AddLocalRpcMethod(rpcDelegate.GetMethodInfo(), rpcDelegate.Target, rpcAttribute) 92 | ) 93 | 94 | serverRpc.StartListening() 95 | let create (s: Stream) : JsonRpc = JsonRpc.Attach(s, target = null) 96 | let clientRpc: JsonRpc = create client 97 | 98 | try 99 | let! (_: Foo) = 100 | clientRpc.InvokeWithParameterObjectAsync("lol", {| bar = "lol" |}) 101 | |> Async.AwaitTask 102 | 103 | () 104 | with Flatten(:? RemoteInvocationException as ex) -> 105 | Expect.equal 106 | ex.Message 107 | "Required property 'baz' not found in JSON. Path ''." 108 | "Get parse error message and not crash process" 109 | 110 | Expect.equal ex.ErrorCode ((int) JsonRpcErrorCode.ParseError) "Should get parse error code" 111 | } 112 | ] -------------------------------------------------------------------------------- /tests/Utils.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Ionide.LanguageServerProtocol.Tests.Utils 3 | 4 | open Ionide.LanguageServerProtocol.Types 5 | open System.Reflection 6 | open Newtonsoft.Json 7 | open System.Collections.Generic 8 | open Newtonsoft.Json.Linq 9 | open System 10 | open Expecto 11 | open System.Collections 12 | open System.Runtime.CompilerServices 13 | 14 | let inline mkPos l c = { Line = l; Character = c } 15 | let inline mkRange s e = { Start = s; End = e } 16 | let inline mkRange' (sl, sc) (el, ec) = mkRange (mkPos sl sc) (mkPos el ec) 17 | let inline mkPosRange l c = mkRange (mkPos l c) (mkPos l c) 18 | 19 | module Lsp = 20 | let internal example = typeof 21 | 22 | let internal path = 23 | let name = example.FullName 24 | let i = name.IndexOf ".TextDocumentIdentifier" 25 | name.Substring(0, i + 1) 26 | 27 | module Is = 28 | /// Not directly in `Ionide.LanguageServerProtocol.Types`, but nested deeper 29 | /// 30 | /// For example cases of unions are nested inside their corresponding union 31 | /// 32 | /// Note: don't confuse with `ty.IsNested`: 33 | /// Modules are static classes and everything inside them is nested inside module 34 | /// -> `IsNested` always true for types inside module 35 | let Nested (ty: Type) = 36 | ty.DeclaringType 37 | <> example.DeclaringType 38 | 39 | /// Generics like `U2<_,_>` or `U2` 40 | /// 41 | /// Note: doesn't differentiate between generic type def and generic type (`U2<_,_>` vs `U2`) 42 | let Generic (ty: Type) = ty.IsGenericType 43 | 44 | /// Generic type defs like `U2<_,_>` 45 | /// 46 | /// Note: unlike `generic`, only excludes actual generic type definitions (`U2<_,_>` but not `U2`) 47 | let GenericTypeDef (ty: Type) = ty.IsGenericTypeDefinition 48 | 49 | /// Note: Union type are abstract: `U2<_,_>` -> `U2` is abstract, while it's cases are concrete types 50 | let Abstract (ty: Type) = ty.IsAbstract 51 | 52 | /// Abstract & Sealed 53 | /// 54 | /// Note: Always excludes -> this rule does nothing 55 | let Static (ty: Type) = 56 | ty.IsAbstract 57 | && ty.IsSealed 58 | 59 | /// Lsp Type: inside `Ionide.LanguageServerProtocol.Types` 60 | /// 61 | /// AdditionalRules: use rules inside `Lsp` module 62 | let isLspType (additionalRules: (Type -> bool) list) (ty: Type) = 63 | ty.FullName.StartsWith Lsp.path 64 | && not ( 65 | // private or internal 66 | (ty.IsNestedPrivate 67 | || ty.IsNestedAssembly) 68 | || ty.IsInterface 69 | || 70 | // static -> modules 71 | (ty.IsAbstract 72 | && ty.IsSealed) 73 | || ty.BaseType = typeof 74 | ) 75 | && (additionalRules 76 | |> List.forall (fun rule -> rule ty)) 77 | 78 | /// Replaces contents of properties with `JsonExtensionData`Attribute of type `IDictionary` 79 | /// with a `Map` containing same elements. 80 | /// `null` field is kept. 81 | /// 82 | /// Note: Mutates `o`! 83 | /// 84 | /// Note: Only converts `JsonExtensionData`. 85 | /// For all other cases: `Map` works just fine. And not using `Map` is probably incorrect. 86 | /// 87 | /// **Use Case**: 88 | /// Dictionaries don't use structural identity 89 | /// -> `Expecto.equal` fails with two dictionaries -- or objects that contain dictionary 90 | /// 91 | /// In Newtonsoft.Json: `JsonExtensionData` required a Dictionary (IDictionary). 92 | /// Unfortunately F# `Map` cannot be used because no default ctor and not mutable. 93 | /// -> Must use something else -- like `Dictionary` (see LSP Type `FormattingOptions`) 94 | /// But now LSP data cannot be compared with `=` or `Expecto.equal`. 95 | /// And the type with `Dictionary` might be nested deep down. 96 | /// 97 | /// -> Replace all dictionaries with `Map` -> comparison works again 98 | let rec convertExtensionDataDictionariesToMap (o: obj) = 99 | let isGenericTypeOf expected (actual: Type) = 100 | actual.IsGenericType 101 | && actual.GetGenericTypeDefinition() = expected 102 | 103 | let (|IsGenericType|_|) expected actual = 104 | if 105 | actual 106 | |> isGenericTypeOf expected 107 | then 108 | Some() 109 | else 110 | None 111 | 112 | match o with 113 | | null -> () 114 | | :? string 115 | | :? bool 116 | | :? int 117 | | :? float 118 | | :? byte 119 | | :? uint -> () 120 | | :? JToken -> () 121 | | :? IDictionary as dict -> 122 | for kv in 123 | dict 124 | |> Seq.cast do 125 | convertExtensionDataDictionariesToMap kv.Value 126 | | :? IEnumerable as ls -> 127 | for v in ls do 128 | convertExtensionDataDictionariesToMap v 129 | | :? ITuple as t -> 130 | for i in 131 | 0 .. (t.Length 132 | - 1) do 133 | convertExtensionDataDictionariesToMap (t[i]) 134 | | _ when 135 | let ty = o.GetType() 136 | 137 | isLspType [] ty 138 | || ty 139 | |> isGenericTypeOf typedefof<_ option> 140 | || ty.FullName.StartsWith "Ionide.LanguageServerProtocol.Tests.Utils+TestData+" 141 | -> 142 | let ty = o.GetType() 143 | 144 | let props = 145 | ty.GetProperties( 146 | BindingFlags.Instance 147 | ||| BindingFlags.Public 148 | ) 149 | 150 | let propsWithValues = 151 | props 152 | |> Seq.choose (fun prop -> 153 | try 154 | let v = prop.GetValue o 155 | 156 | (prop, v) 157 | |> Some 158 | with ex -> 159 | failwithf "Couldn't get value of %s in %A: %s" prop.Name ty ex.Message 160 | None 161 | ) 162 | 163 | for (prop, value) in propsWithValues do 164 | match value with 165 | | null -> () 166 | | :? Map -> () 167 | | :? IDictionary as dict -> 168 | match prop.GetCustomAttribute() with 169 | | null -> () 170 | | _ when not prop.CanWrite -> 171 | // assumption: Dictionary is mutable 172 | // otherwise: can be handled by getting and setting backing field 173 | // but not done here (for serializing in record: must be mutable set after in `OnDeserialized` to prevent null) 174 | () 175 | | _ -> 176 | let v = 177 | dict 178 | |> Seq.map (fun kv -> kv.Key, kv.Value) 179 | |> Map.ofSeq 180 | 181 | prop.SetValue(o, v) 182 | | _ -> convertExtensionDataDictionariesToMap value 183 | | _ -> () 184 | 185 | 186 | module TestData = 187 | type WithExtensionData = { 188 | Value: string 189 | [] 190 | mutable AdditionalData: IDictionary 191 | } 192 | 193 | [] 194 | type MyUnion = 195 | | Case1 of string 196 | | Case2 of WithExtensionData 197 | | Case3 of string * WithExtensionData * int 198 | 199 | [] 200 | type MyContainer = 201 | | Fin 202 | | Datum of WithExtensionData * MyContainer 203 | | Data of WithExtensionData[] * MyContainer 204 | | Big of MyContainer[] 205 | | Text of string * MyContainer 206 | 207 | let tests = 208 | testList "test utils" [ 209 | testList (nameof isLspType) [ 210 | testCase "string isn't lsp type" 211 | <| fun _ -> 212 | let isLsp = 213 | typeof 214 | |> isLspType [] 215 | 216 | Expect.isFalse isLsp "string isn't lsp" 217 | testCase "DocumentLink is lsp type" 218 | <| fun _ -> 219 | let isLsp = 220 | typeof 221 | |> isLspType [] 222 | 223 | Expect.isTrue isLsp "DocumentLink is lsp" 224 | testCase "DocumentLink is direct, non-generic lsp type" 225 | <| fun _ -> 226 | let isLsp = 227 | typeof 228 | |> isLspType [ 229 | not 230 | << Lsp.Is.Nested 231 | not 232 | << Lsp.Is.Generic 233 | ] 234 | 235 | Expect.isTrue isLsp "DocumentLink is lsp" 236 | 237 | testCase "U2 is lsp type" 238 | <| fun _ -> 239 | let isLsp = 240 | typeof> 241 | |> isLspType [] 242 | 243 | Expect.isTrue isLsp "U2 is lsp" 244 | testCase "U2 is not non-generic lsp type" 245 | <| fun _ -> 246 | let isLsp = 247 | typeof> 248 | |> isLspType [ 249 | not 250 | << Lsp.Is.Generic 251 | ] 252 | 253 | Expect.isFalse isLsp "U2 is generic lsp" 254 | testCase "U2 is non-generic-type-def lsp type" 255 | <| fun _ -> 256 | let isLsp = 257 | typeof> 258 | |> isLspType [ 259 | not 260 | << Lsp.Is.GenericTypeDef 261 | ] 262 | 263 | Expect.isTrue isLsp "U2 is not generic type def lsp" 264 | testCase "U2<_,_> is not non-generic lsp type" 265 | <| fun _ -> 266 | let isLsp = 267 | typedefof> 268 | |> isLspType [ 269 | not 270 | << Lsp.Is.Generic 271 | ] 272 | 273 | Expect.isFalse isLsp "U2 is generic lsp" 274 | testCase "U2<_,_> is not non-generic-type-def lsp type" 275 | <| fun _ -> 276 | let isLsp = 277 | typedefof> 278 | |> isLspType [ 279 | not 280 | << Lsp.Is.GenericTypeDef 281 | ] 282 | 283 | Expect.isFalse isLsp "U2 is generic type def lsp" 284 | testCase "U2<_,_> is not non-abstract lsp type" 285 | <| fun _ -> 286 | let isLsp = 287 | typedefof> 288 | |> isLspType [ 289 | not 290 | << Lsp.Is.Abstract 291 | ] 292 | 293 | Expect.isFalse isLsp "U2 is abstract lsp" 294 | 295 | testCase "MarkedString.String is lsp" 296 | <| fun _ -> 297 | let o = U2.C1 "foo" 298 | 299 | let isLsp = 300 | o.GetType() 301 | |> isLspType [] 302 | 303 | Expect.isTrue isLsp "MarkedString.String is lsp" 304 | testCase "MarkedString.String isn't direct lsp" 305 | <| fun _ -> 306 | let o = U2.C1 "foo" 307 | 308 | let isLsp = 309 | o.GetType() 310 | |> isLspType [ 311 | not 312 | << Lsp.Is.Nested 313 | ] 314 | 315 | Expect.isFalse isLsp "MarkedString.String is not direct lsp" 316 | 317 | testCase "Client isn't lsp" 318 | <| fun _ -> 319 | let isLsp = 320 | typeof 321 | |> isLspType [] 322 | 323 | Expect.isFalse isLsp "Client isn't lsp" 324 | ] 325 | 326 | testList (nameof convertExtensionDataDictionariesToMap) [ 327 | let testConvert preActual expectedAfterwards = 328 | Expect.notEqual preActual expectedAfterwards "Dictionary and Map shouldn't be equal" 329 | 330 | convertExtensionDataDictionariesToMap preActual 331 | Expect.equal preActual expectedAfterwards "Converter Map should be comparable" 332 | 333 | let dict = 334 | [| 335 | "alpha", JToken.FromObject "lorem" 336 | "beta", JToken.FromObject "ipsum" 337 | "gamma", JToken.FromObject "dolor" 338 | |] 339 | |> Map.ofArray 340 | 341 | let createWithExtensionData () : TestData.WithExtensionData = { 342 | Value = "foo" 343 | AdditionalData = 344 | dict 345 | |> Dictionary 346 | } 347 | 348 | testCase "can convert direct dictionary field" 349 | <| fun _ -> 350 | let actual = createWithExtensionData () 351 | let expected = { actual with AdditionalData = dict } 352 | testConvert actual expected 353 | 354 | testCase "can convert inside union in case with single value" 355 | <| fun _ -> 356 | let extData = createWithExtensionData () 357 | let actual = TestData.MyUnion.Case2 extData 358 | let expected = TestData.MyUnion.Case2 { extData with AdditionalData = dict } 359 | testConvert actual expected 360 | 361 | testCase "can convert inside union in case with multiple values" 362 | <| fun _ -> 363 | let extData = createWithExtensionData () 364 | let actual = TestData.MyUnion.Case3("foo", extData, 42) 365 | let expected = TestData.MyUnion.Case3("foo", { extData with AdditionalData = dict }, 42) 366 | testConvert actual expected 367 | 368 | testCase "can convert in U2" 369 | <| fun _ -> 370 | let extData = createWithExtensionData () 371 | let actual: U2 = U2.C2 extData 372 | let expected: U2 = U2.C2 { extData with AdditionalData = dict } 373 | testConvert actual expected 374 | 375 | testCase "can convert in tuple" 376 | <| fun _ -> 377 | let extData = createWithExtensionData () 378 | let actual = ("foo", extData, 42) 379 | let expected = ("foo", { extData with AdditionalData = dict }, 42) 380 | testConvert actual expected 381 | 382 | testCase "can convert in array" 383 | <| fun _ -> 384 | let extData = createWithExtensionData () 385 | 386 | let actual: obj[] = [| 387 | "foo" 388 | extData 389 | 42 390 | |] 391 | 392 | let expected: obj[] = [| 393 | "foo" 394 | { extData with AdditionalData = dict } 395 | 42 396 | |] 397 | 398 | testConvert actual expected 399 | 400 | testCase "can convert in list" 401 | <| fun _ -> 402 | let extData = createWithExtensionData () 403 | 404 | let actual: obj list = [ 405 | "foo" 406 | extData 407 | 42 408 | ] 409 | 410 | let expected: obj list = [ 411 | "foo" 412 | { extData with AdditionalData = dict } 413 | 42 414 | ] 415 | 416 | testConvert actual expected 417 | 418 | testCase "can convert option" 419 | <| fun _ -> 420 | let extData = createWithExtensionData () 421 | let actual = Some extData 422 | let expected = Some { extData with AdditionalData = dict } 423 | testConvert actual expected 424 | 425 | testCase "replaces all dictionaries" 426 | <| fun _ -> 427 | let extDataMap = 428 | Array.init 429 | 5 430 | (fun i -> 431 | let m = 432 | Array.init (i + 3) (fun j -> ($"Dict{i}Element{j}", JToken.FromObject(i + j))) 433 | |> Map.ofArray 434 | 435 | { 436 | TestData.WithExtensionData.Value = $"Hello {i}" 437 | TestData.WithExtensionData.AdditionalData = m 438 | } 439 | ) 440 | 441 | let extDataDict = 442 | extDataMap 443 | |> Array.map (fun extData -> { extData with AdditionalData = Dictionary extData.AdditionalData }) 444 | 445 | let actual = TestData.MyContainer.Data(extDataDict, TestData.MyContainer.Fin) 446 | let expected = TestData.MyContainer.Data(extDataMap, TestData.MyContainer.Fin) 447 | testConvert actual expected 448 | 449 | testCase "can replace deeply nested" 450 | <| fun _ -> 451 | let createExtensionData mkDict seed : TestData.WithExtensionData = { 452 | Value = $"Seed {seed}" 453 | AdditionalData = 454 | let count = 455 | seed % 4 456 | + 3 457 | 458 | List.init 459 | (seed % 4 460 | + 3) 461 | (fun i -> ($"Seed{seed}Element{i}Of{count}", JToken.FromObject(count + i))) 462 | |> Map.ofList 463 | |> mkDict 464 | } 465 | 466 | /// builds always same object for same depth 467 | let rec buildObject mkDict (depth: int) = 468 | match depth % 4 with 469 | | _ when depth <= 0 -> TestData.MyContainer.Fin 470 | | 0 -> 471 | [| 1 .. (max 3 (depth / 2)) |] 472 | |> Array.map (fun i -> 473 | buildObject 474 | mkDict 475 | (depth 476 | - 1 477 | - (i % 2)) 478 | ) 479 | |> TestData.MyContainer.Big 480 | | 1 -> 481 | let o = buildObject mkDict (depth - 1) 482 | let d = createExtensionData mkDict depth 483 | TestData.MyContainer.Datum(d, o) 484 | | 2 -> 485 | let o = buildObject mkDict (depth - 1) 486 | 487 | let ds = 488 | [| 1 .. max 3 (depth / 2) |] 489 | |> Array.map (fun i -> createExtensionData mkDict (depth * i)) 490 | 491 | TestData.MyContainer.Data(ds, o) 492 | | 3 -> 493 | let o = buildObject mkDict (depth - 1) 494 | let d = $"Depth={depth}" 495 | TestData.MyContainer.Text(d, o) 496 | 497 | | _ -> failwith "unreachable" 498 | 499 | let depth = 7 500 | let expected = buildObject id depth 501 | let actual = buildObject Dictionary depth 502 | testConvert actual expected 503 | ] 504 | ] -------------------------------------------------------------------------------- /tools/MetaModelGenerator/Common.fs: -------------------------------------------------------------------------------- 1 | namespace MetaModelGenerator 2 | 3 | 4 | module FileWriters = 5 | open System.IO 6 | 7 | let writeIfChanged outputPath text = 8 | async { 9 | let writeToFile (path: string) (contents: string) = File.WriteAllTextAsync(path, contents) 10 | 11 | let! existingFile = 12 | async { 13 | if File.Exists(outputPath) then 14 | let! file = 15 | File.ReadAllTextAsync(outputPath) 16 | |> Async.AwaitTask 17 | 18 | return Some file 19 | else 20 | return None 21 | } 22 | 23 | printfn "Writing to %s" outputPath 24 | 25 | match existingFile with 26 | | Some existingFile when existingFile = text -> printfn "No changes" 27 | | _ -> 28 | do! 29 | text 30 | |> writeToFile outputPath 31 | |> Async.AwaitTask 32 | } 33 | 34 | 35 | module Widgets = 36 | [] 37 | let UriString = "URI" 38 | 39 | [] 40 | let DocumentUriString = "DocumentUri" 41 | 42 | [] 43 | let RegExpString = "RegExp" 44 | 45 | 46 | [] 47 | module TypeAnonBuilders = 48 | open Fabulous.AST 49 | open Fantomas.Core.SyntaxOak 50 | 51 | let pipe (right: WidgetBuilder) (left: WidgetBuilder) = Ast.InfixAppExpr(left, "|>", right) 52 | 53 | 54 | type Ast with 55 | 56 | static member LspUri() = Ast.LongIdent Widgets.DocumentUriString 57 | static member DocumentUri() = Ast.LongIdent Widgets.DocumentUriString 58 | static member LspRegExp() = Ast.LongIdent Widgets.DocumentUriString 59 | 60 | static member AsyncPrefix(t: WidgetBuilder) = Ast.AppPrefix(Ast.LongIdent "Async", [ t ]) 61 | 62 | static member AsyncLspResultPrefix(t: WidgetBuilder) = Ast.AppPrefix(Ast.LongIdent "AsyncLspResult", [ t ]) -------------------------------------------------------------------------------- /tools/MetaModelGenerator/GenerateClientServer.fs: -------------------------------------------------------------------------------- 1 | namespace MetaModelGenerator 2 | 3 | module GenerateClientServer = 4 | open System 5 | open Fantomas.Core.SyntaxOak 6 | open Fabulous.AST 7 | open type Fabulous.AST.Ast 8 | open Fantomas.Core 9 | 10 | 11 | let generateClientServer (parsedMetaModel: MetaModel.MetaModel) outputPath = 12 | async { 13 | printfn "Generating generateClientServer" 14 | 15 | 16 | let requests = 17 | parsedMetaModel.Requests 18 | |> Array.filter Proposed.checkProposed 19 | |> Array.groupBy (fun x -> x.MessageDirection) 20 | |> Map 21 | 22 | let notifications = 23 | parsedMetaModel.Notifications 24 | |> Array.filter Proposed.checkProposed 25 | |> Array.groupBy (fun x -> x.MessageDirection) 26 | |> Map 27 | 28 | 29 | let serverRequests = [ 30 | yield! 31 | requests 32 | |> Map.tryFind MetaModel.MessageDirection.ClientToServer 33 | |> Option.defaultValue [||] 34 | yield! 35 | requests 36 | |> Map.tryFind MetaModel.MessageDirection.Both 37 | |> Option.defaultValue [||] 38 | ] 39 | 40 | let serverNotifications = [ 41 | yield! 42 | notifications 43 | |> Map.tryFind MetaModel.MessageDirection.ClientToServer 44 | |> Option.defaultValue [||] 45 | yield! 46 | notifications 47 | |> Map.tryFind MetaModel.MessageDirection.Both 48 | |> Option.defaultValue [||] 49 | ] 50 | 51 | 52 | let clientRequests = [ 53 | yield! 54 | requests 55 | |> Map.tryFind MetaModel.MessageDirection.ServerToClient 56 | |> Option.defaultValue [||] 57 | yield! 58 | requests 59 | |> Map.tryFind MetaModel.MessageDirection.Both 60 | |> Option.defaultValue [||] 61 | ] 62 | 63 | let clientNotifications = [ 64 | yield! 65 | notifications 66 | |> Map.tryFind MetaModel.MessageDirection.ServerToClient 67 | |> Option.defaultValue [||] 68 | yield! 69 | notifications 70 | |> Map.tryFind MetaModel.MessageDirection.Both 71 | |> Option.defaultValue [||] 72 | ] 73 | 74 | let normalizeMethod (s: string) = 75 | let parts = 76 | s.Split( 77 | "/", 78 | StringSplitOptions.RemoveEmptyEntries 79 | ||| StringSplitOptions.TrimEntries 80 | ) 81 | 82 | parts 83 | |> Array.filter (fun x -> x <> "$") 84 | |> Array.map (fun x -> 85 | (string x.[0]).ToUpper() 86 | + x.[1..] 87 | ) 88 | |> String.concat "" 89 | 90 | let oak = 91 | Ast.Oak() { 92 | Namespace "Ionide.LanguageServerProtocol" { 93 | Open "Ionide.LanguageServerProtocol.Types" 94 | Open "Ionide.LanguageServerProtocol.JsonRpc" 95 | 96 | let generateInterface 97 | name 98 | (notifications: list) 99 | (requests: list) 100 | = 101 | 102 | 103 | TypeDefn name { 104 | 105 | Inherit "System.IDisposable" 106 | 107 | let notificationComment = SingleLine "Notifications" 108 | 109 | let mutable writtenNotificationComment = false 110 | 111 | for n in notifications do 112 | let methodName = normalizeMethod n.Method 113 | 114 | let parameters = [ 115 | match n.Params with 116 | | None -> yield Unit() 117 | | Some ps -> 118 | for p in ps do 119 | match p with 120 | | MetaModel.Type.ReferenceType r -> yield LongIdent r.Name 121 | | _ -> () 122 | ] 123 | 124 | let returnType = AsyncPrefix(Unit()) 125 | 126 | 127 | let wb = AbstractMember(methodName, parameters, returnType) 128 | 129 | let wb = 130 | n.StructuredDocs 131 | |> Option.mapOrDefault wb wb.xmlDocs 132 | 133 | let wb = 134 | if not writtenNotificationComment then 135 | writtenNotificationComment <- true 136 | wb.triviaBefore notificationComment 137 | else 138 | wb 139 | 140 | wb 141 | 142 | let requestComment = SingleLine "Requests" 143 | 144 | let mutable writtenRequestComment = false 145 | 146 | 147 | for r in requests do 148 | let methodName = normalizeMethod r.Method 149 | 150 | let parameters = [ 151 | match r.Params with 152 | | None -> yield Unit() 153 | | Some ps -> 154 | for p in ps do 155 | match p with 156 | | MetaModel.Type.ReferenceType r -> yield (LongIdent r.Name) 157 | | _ -> () 158 | ] 159 | 160 | let returnType = 161 | let rec returnType (ty: MetaModel.Type) = 162 | match ty with 163 | | MetaModel.Type.ReferenceType r -> LongIdent r.Name 164 | | MetaModel.Type.BaseType b -> 165 | match b.Name with 166 | | MetaModel.BaseTypes.Null -> Unit() 167 | | MetaModel.BaseTypes.Boolean -> Boolean() 168 | | MetaModel.BaseTypes.Integer -> Int() 169 | | MetaModel.BaseTypes.Uinteger -> UInt32() 170 | | MetaModel.BaseTypes.Decimal -> Float() 171 | | MetaModel.BaseTypes.String -> String() 172 | | MetaModel.BaseTypes.DocumentUri -> DocumentUri() 173 | | MetaModel.BaseTypes.Uri -> LspUri() 174 | | MetaModel.BaseTypes.RegExp -> LspRegExp() 175 | | MetaModel.Type.OrType o -> 176 | // TS types can have optional properties (myKey?: string) 177 | // and unions with null (string | null) 178 | // we need to handle both cases 179 | let isOptional, items = 180 | if Array.exists MetaModel.isNullableType o.Items then 181 | true, 182 | o.Items 183 | |> Array.filter (fun x -> not (MetaModel.isNullableType x)) 184 | else 185 | false, o.Items 186 | 187 | let types = 188 | items 189 | |> Array.map returnType 190 | |> Array.toList 191 | 192 | let retType = 193 | if types.Length > 1 then 194 | let duType = $"U{types.Length}" 195 | AppPrefix(duType, types) 196 | else 197 | types.[0] 198 | 199 | if isOptional then OptionPrefix retType else retType 200 | | MetaModel.Type.ArrayType a -> ArrayPrefix(returnType a.Element) 201 | | _ -> LongIdent "Unsupported Type" 202 | 203 | AsyncLspResultPrefix(returnType r.Result) 204 | 205 | let wb = AbstractMember(methodName, parameters, returnType) 206 | 207 | let wb = 208 | r.StructuredDocs 209 | |> Option.mapOrDefault wb wb.xmlDocs 210 | 211 | let wb = 212 | if not writtenRequestComment then 213 | writtenRequestComment <- true 214 | wb.triviaBefore (requestComment) 215 | else 216 | wb 217 | 218 | wb 219 | } 220 | 221 | generateInterface "ILspServer" serverNotifications serverRequests 222 | generateInterface "ILspClient" clientNotifications clientRequests 223 | 224 | let generateServerRequestHandlingRecord = 225 | let serverTypeArg = "'server" 226 | 227 | Record "ServerRequestHandling" { Field("Run", Funs([ serverTypeArg ], "System.Delegate")) } 228 | |> _.typeParams(PostfixList(TyparDecl(serverTypeArg), SubtypeOf(serverTypeArg, LongIdent("ILspServer")))) 229 | 230 | let generateRoutes = 231 | 232 | let body = 233 | let generateRoute requestParams (method: string) configureValue = 234 | let callWith = 235 | if Array.isEmpty requestParams then 236 | ParenExpr "" 237 | else 238 | ParenExpr "request" 239 | 240 | TupleExpr [ 241 | 242 | ConstantExpr(String method) 243 | 244 | AppWithLambdaExpr( 245 | ConstantExpr "serverRequestHandling", 246 | [ 247 | ConstantPat "server" 248 | ConstantPat "request" 249 | ], 250 | AppLongIdentAndSingleParenArgExpr( 251 | [ 252 | "server" 253 | normalizeMethod method 254 | ], 255 | callWith 256 | ) 257 | |> configureValue 258 | ) 259 | ] 260 | 261 | let generateRouteHandler = 262 | LetOrUseExpr( 263 | Function( 264 | "serverRequestHandling", 265 | NamedPat "run", 266 | RecordExpr [ 267 | RecordFieldExpr( 268 | "Run", 269 | LambdaExpr( 270 | [ ParameterPat "server" ], 271 | AppLongIdentAndSingleParenArgExpr([ "run" ], ConstantExpr "server") 272 | |> pipe (ConstantExpr "JsonRpc.Requests.requestHandling") 273 | ) 274 | ) 275 | ] 276 | ) 277 | ) 278 | 279 | CompExprBodyExpr [ 280 | generateRouteHandler 281 | OtherExpr( 282 | ListExpr [ 283 | for serverRequest in serverRequests do 284 | generateRoute serverRequest.ParamsSafe serverRequest.Method id 285 | 286 | for serverNotification in serverNotifications do 287 | generateRoute 288 | serverNotification.ParamsSafe 289 | serverNotification.Method 290 | (pipe (ConstantExpr "Requests.notificationSuccess")) 291 | 292 | 293 | ] 294 | ) 295 | ] 296 | 297 | 298 | Function("routeMappings", [ UnitPat() ], body) 299 | 300 | Module "Mappings" { 301 | 302 | 303 | generateServerRequestHandlingRecord 304 | generateRoutes 305 | 306 | } 307 | 308 | } 309 | } 310 | 311 | 312 | let! formattedText = 313 | oak 314 | |> Gen.mkOak 315 | |> CodeFormatter.FormatOakAsync 316 | 317 | do! FileWriters.writeIfChanged outputPath formattedText 318 | 319 | } -------------------------------------------------------------------------------- /tools/MetaModelGenerator/MetaModel.fs: -------------------------------------------------------------------------------- 1 | namespace MetaModelGenerator 2 | 3 | 4 | type StructuredDocs = string list 5 | 6 | module StructuredDocs = 7 | let parse (s: string) = 8 | s.Trim('\n').Split([| '\n' |]) 9 | |> Array.toList 10 | 11 | module Proposed = 12 | let skipProposed = true 13 | 14 | let inline checkProposed x = 15 | if skipProposed then 16 | (^a: (member Proposed: bool option) x) 17 | <> Some true 18 | else 19 | true 20 | 21 | module Preconverts = 22 | open Newtonsoft.Json 23 | open Newtonsoft.Json.Linq 24 | 25 | type SingleOrArrayConverter<'T>() = 26 | inherit JsonConverter() 27 | 28 | override x.CanConvert(tobject: System.Type) = tobject = typeof<'T array> 29 | 30 | override _.WriteJson(writer: JsonWriter, value, serializer: JsonSerializer) : unit = 31 | failwith "Should never be writing this structure, it comes from Microsoft LSP Spec" 32 | 33 | override _.ReadJson(reader: JsonReader, objectType: System.Type, existingValue: obj, serializer: JsonSerializer) = 34 | let token = JToken.Load reader 35 | 36 | match token.Type with 37 | | JTokenType.Array -> serializer.Deserialize(reader, objectType) 38 | | JTokenType.Null -> null 39 | | _ -> Some [| token.ToObject<'T>(serializer) |] 40 | 41 | module rec MetaModel = 42 | open System 43 | open Newtonsoft.Json.Linq 44 | open Newtonsoft.Json 45 | open Ionide.LanguageServerProtocol 46 | 47 | type MetaData = { Version: string } 48 | 49 | /// Indicates in which direction a message is sent in the protocol. 50 | [)>] 51 | type MessageDirection = 52 | | ClientToServer = 0 53 | | ServerToClient = 1 54 | | Both = 2 55 | 56 | /// Represents a LSP request 57 | type Request = { 58 | /// Whether the request is deprecated or not. If deprecated the property contains the deprecation message." 59 | Deprecated: string option 60 | /// An optional documentation; 61 | Documentation: string option 62 | /// An optional error data type. 63 | ErrorData: Type option 64 | /// The direction in which this request is sent in the protocol. 65 | MessageDirection: MessageDirection 66 | /// The request's method name. 67 | Method: string 68 | /// The parameter type(s) if any. 69 | [>)>] 70 | Params: Type array option 71 | /// Optional partial result type if the request supports partial result reporting. 72 | PartialResult: Type option 73 | /// Whether this is a proposed feature. If omitted the feature is final.", 74 | Proposed: bool option 75 | /// Optional a dynamic registration method if it different from the request's method." 76 | RegistrationMethod: string option 77 | /// Optional registration options if the request supports dynamic registration." 78 | RegistrationOptions: Type option 79 | /// The result type. 80 | Result: Type 81 | /// Since when (release number) this request is available. Is undefined if not known.", 82 | Since: string option 83 | } with 84 | 85 | member x.ParamsSafe = 86 | x.Params 87 | |> Option.Array.toArray 88 | 89 | member x.StructuredDocs = 90 | x.Documentation 91 | |> Option.map StructuredDocs.parse 92 | 93 | /// Represents a LSP notification 94 | type Notification = { 95 | 96 | /// Whether the notification is deprecated or not. If deprecated the property contains the deprecation message." 97 | Deprecated: string option 98 | /// An optional documentation; 99 | Documentation: string option 100 | /// The direction in which this notification is sent in the protocol. 101 | MessageDirection: MessageDirection 102 | /// The request's method name. 103 | Method: string 104 | /// The parameter type(s) if any. 105 | [>)>] 106 | Params: Type array option 107 | /// Whether this is a proposed feature. If omitted the notification is final.", 108 | Proposed: bool option 109 | /// Optional a dynamic registration method if it different from the request's method." 110 | RegistrationMethod: string option 111 | /// Optional registration options if the notification supports dynamic registration." 112 | RegistrationOptions: Type option 113 | /// Since when (release number) this notification is available. Is undefined if not known.", 114 | Since: string option 115 | } with 116 | 117 | member x.ParamsSafe = 118 | x.Params 119 | |> Option.Array.toArray 120 | 121 | member x.StructuredDocs = 122 | x.Documentation 123 | |> Option.map StructuredDocs.parse 124 | 125 | [] 126 | type BaseTypes = 127 | | Uri 128 | | DocumentUri 129 | | Integer 130 | | Uinteger 131 | | Decimal 132 | | RegExp 133 | | String 134 | | Boolean 135 | | Null 136 | 137 | static member Parse(s: string) = 138 | match s with 139 | | "URI" -> Uri 140 | | "DocumentUri" -> DocumentUri 141 | | "integer" -> Integer 142 | | "uinteger" -> Uinteger 143 | | "decimal" -> Decimal 144 | | "RegExp" -> RegExp 145 | | "string" -> String 146 | | "boolean" -> Boolean 147 | | "null" -> Null 148 | | _ -> failwithf "Unknown base type: %s" s 149 | 150 | member x.ToDotNetType() = 151 | match x with 152 | | Uri -> "URI" 153 | | DocumentUri -> "DocumentUri" 154 | | Integer -> "int32" 155 | | Uinteger -> "uint32" 156 | | Decimal -> "decimal" 157 | | RegExp -> "RegExp" 158 | | String -> "string" 159 | | Boolean -> "bool" 160 | | Null -> "null" 161 | 162 | [] 163 | let BaseTypeConst = "base" 164 | 165 | type BaseType = { Kind: string; Name: BaseTypes } 166 | 167 | [] 168 | let ReferenceTypeConst = "reference" 169 | 170 | type ReferenceType = { Kind: string; Name: string } 171 | 172 | [] 173 | let ArrayTypeConst = "array" 174 | 175 | type ArrayType = { Kind: string; Element: Type } 176 | 177 | [] 178 | let MapTypeConst = "map" 179 | 180 | type MapType = { Kind: string; Key: MapKeyType; Value: Type } 181 | 182 | [] 183 | type MapKeyNameEnum = 184 | | Uri 185 | | DocumentUri 186 | | String 187 | | Integer 188 | 189 | static member Parse(s: string) = 190 | match s with 191 | | "URI" -> Uri 192 | | "DocumentUri" -> DocumentUri 193 | | "string" -> String 194 | | "integer" -> Integer 195 | | _ -> failwithf "Unknown map key name: %s" s 196 | 197 | member x.ToDotNetType() = 198 | match x with 199 | | Uri -> "URI" 200 | | DocumentUri -> "DocumentUri" 201 | | String -> "string" 202 | | Integer -> "int32" 203 | 204 | [] 205 | type MapKeyType = 206 | | ReferenceType of ReferenceType 207 | | Base of {| Kind: string; Name: MapKeyNameEnum |} 208 | 209 | [] 210 | let AndTypeConst = "and" 211 | 212 | type AndType = { Kind: string; Items: Type array } 213 | 214 | [] 215 | let OrTypeConst = "or" 216 | 217 | type OrType = { Kind: string; Items: Type array } 218 | 219 | [] 220 | let TupleTypeConst = "tuple" 221 | 222 | type TupleType = { Kind: string; Items: Type array } 223 | 224 | type Property = { 225 | Deprecated: string option 226 | Documentation: string option 227 | Name: string 228 | Optional: bool option 229 | Proposed: bool option 230 | Required: bool option 231 | Since: string option 232 | Type: Type 233 | } with 234 | 235 | member x.IsOptional = 236 | x.Optional 237 | |> Option.defaultValue false 238 | 239 | member x.NameAsPascalCase = String.toPascalCase x.Name 240 | 241 | member x.StructuredDocs = 242 | x.Documentation 243 | |> Option.map StructuredDocs.parse 244 | 245 | 246 | [] 247 | let StructureTypeLiteral = "literal" 248 | 249 | type StructureLiteral = { 250 | Deprecated: string option 251 | Documentation: string option 252 | Properties: Property array 253 | Proposed: bool option 254 | Since: string option 255 | } with 256 | 257 | member x.StructuredDocs = 258 | x.Documentation 259 | |> Option.map StructuredDocs.parse 260 | 261 | member x.PropertiesSafe = 262 | x.Properties 263 | |> Array.filter Proposed.checkProposed 264 | 265 | type StructureLiteralType = { Kind: string; Value: StructureLiteral } 266 | 267 | [] 268 | let StringLiteralTypeConst = "stringLiteral" 269 | 270 | type StringLiteralType = { Kind: string; Value: string } 271 | 272 | [] 273 | let IntegerLiteralTypeConst = "integerLiteral" 274 | 275 | type IntegerLiteralType = { Kind: string; Value: decimal } 276 | 277 | [] 278 | let BooleanLiteralTypeConst = "booleanLiteral" 279 | 280 | type BooleanLiteralType = { Kind: string; Value: bool } 281 | 282 | [] 283 | type Type = 284 | | BaseType of BaseType 285 | | ReferenceType of ReferenceType 286 | | ArrayType of ArrayType 287 | | MapType of MapType 288 | | AndType of AndType 289 | | OrType of OrType 290 | | TupleType of TupleType 291 | | StructureLiteralType of StructureLiteralType 292 | | StringLiteralType of StringLiteralType 293 | | IntegerLiteralType of IntegerLiteralType 294 | | BooleanLiteralType of BooleanLiteralType 295 | 296 | member x.isStructureLiteralType = 297 | match x with 298 | | StructureLiteralType _ -> true 299 | | _ -> false 300 | 301 | 302 | type Structure = { 303 | Deprecated: string option 304 | Documentation: string option 305 | Extends: Type array option 306 | Mixins: Type array option 307 | Name: string 308 | Properties: Property array option 309 | Proposed: bool option 310 | Since: string option 311 | } with 312 | 313 | member x.ExtendsSafe = Option.Array.toArray x.Extends 314 | member x.MixinsSafe = Option.Array.toArray x.Mixins 315 | 316 | member x.PropertiesSafe = 317 | Option.Array.toArray x.Properties 318 | |> Seq.filter Proposed.checkProposed 319 | 320 | member x.StructuredDocs = 321 | x.Documentation 322 | |> Option.map StructuredDocs.parse 323 | 324 | type TypeAlias = { 325 | Deprecated: string option 326 | Documentation: string option 327 | Name: string 328 | Proposed: bool option 329 | Since: string option 330 | Type: Type 331 | } with 332 | 333 | member x.StructuredDocs = 334 | x.Documentation 335 | |> Option.map StructuredDocs.parse 336 | 337 | [)>] 338 | type EnumerationTypeNameValues = 339 | | String = 0 340 | | Integer = 1 341 | | Uinteger = 2 342 | 343 | type EnumerationType = { Kind: string; Name: EnumerationTypeNameValues } 344 | 345 | type EnumerationEntry = { 346 | Deprecated: string option 347 | Documentation: string option 348 | 349 | Name: string 350 | Proposed: bool option 351 | Since: string option 352 | Value: string 353 | } with 354 | 355 | member x.StructuredDocs = 356 | x.Documentation 357 | |> Option.map StructuredDocs.parse 358 | 359 | type Enumeration = { 360 | Deprecated: string option 361 | Documentation: string option 362 | Name: string 363 | Proposed: bool option 364 | Since: string option 365 | SupportsCustomValues: bool option 366 | Type: EnumerationType 367 | Values: EnumerationEntry array 368 | } with 369 | 370 | member x.StructuredDocs = 371 | x.Documentation 372 | |> Option.map StructuredDocs.parse 373 | 374 | member x.ValuesSafe = 375 | x.Values 376 | |> Array.filter Proposed.checkProposed 377 | 378 | type MetaModel = { 379 | MetaData: MetaData 380 | Requests: Request array 381 | Notifications: Notification array 382 | Structures: Structure array 383 | TypeAliases: TypeAlias array 384 | Enumerations: Enumeration array 385 | } with 386 | 387 | member x.StructuresSafe = 388 | x.Structures 389 | |> Array.filter Proposed.checkProposed 390 | 391 | member x.TypeAliasesSafe = 392 | x.TypeAliases 393 | |> Array.filter Proposed.checkProposed 394 | 395 | member x.EnumerationsSafe = 396 | x.Enumerations 397 | |> Array.filter Proposed.checkProposed 398 | 399 | module Converters = 400 | 401 | type MapKeyTypeConverter() = 402 | inherit JsonConverter() 403 | 404 | override _.WriteJson(writer: JsonWriter, value: MapKeyType, serializer: JsonSerializer) : unit = 405 | failwith "Should never be writing this structure, it comes from Microsoft LSP Spec" 406 | 407 | override _.ReadJson 408 | ( 409 | reader: JsonReader, 410 | objectType: System.Type, 411 | existingValue: MapKeyType, 412 | hasExistingValue, 413 | serializer: JsonSerializer 414 | ) = 415 | let jobj = JObject.Load(reader) 416 | let kind = jobj.["kind"].Value() 417 | 418 | match kind with 419 | | ReferenceTypeConst -> 420 | let name = jobj.["name"].Value() 421 | MapKeyType.ReferenceType { Kind = kind; Name = name } 422 | | "base" -> 423 | let name = jobj.["name"].Value() 424 | MapKeyType.Base {| Kind = kind; Name = MapKeyNameEnum.Parse name |} 425 | | _ -> failwithf "Unknown map key type: %s" kind 426 | 427 | type TypeConverter() = 428 | inherit JsonConverter() 429 | 430 | override _.WriteJson(writer: JsonWriter, value: MetaModel.Type, serializer: JsonSerializer) : unit = 431 | failwith "Should never be writing this structure, it comes from Microsoft LSP Spec" 432 | 433 | override _.ReadJson 434 | (reader: JsonReader, objectType: System.Type, existingValue: Type, hasExistingValue, serializer: JsonSerializer) 435 | = 436 | let jobj = JObject.Load(reader) 437 | let kind = jobj.["kind"].Value() 438 | 439 | match kind with 440 | | BaseTypeConst -> 441 | let name = jobj.["name"].Value() 442 | Type.BaseType { Kind = kind; Name = BaseTypes.Parse name } 443 | | ReferenceTypeConst -> 444 | let name = jobj.["name"].Value() 445 | Type.ReferenceType { Kind = kind; Name = name } 446 | | ArrayTypeConst -> 447 | let element = jobj.["element"].ToObject(serializer) 448 | Type.ArrayType { Kind = kind; Element = element } 449 | | MapTypeConst -> 450 | let key = jobj.["key"].ToObject(serializer) 451 | let value = jobj.["value"].ToObject(serializer) 452 | Type.MapType { Kind = kind; Key = key; Value = value } 453 | | AndTypeConst -> 454 | let items = jobj.["items"].ToObject(serializer) 455 | Type.AndType { Kind = kind; Items = items } 456 | | OrTypeConst -> 457 | let items = jobj.["items"].ToObject(serializer) 458 | Type.OrType { Kind = kind; Items = items } 459 | | TupleTypeConst -> 460 | let items = jobj.["items"].ToObject(serializer) 461 | Type.TupleType { Kind = kind; Items = items } 462 | | StructureTypeLiteral -> 463 | let value = jobj.["value"].ToObject(serializer) 464 | Type.StructureLiteralType { Kind = kind; Value = value } 465 | | StringLiteralTypeConst -> 466 | let value = jobj.["value"].Value() 467 | Type.StringLiteralType { Kind = kind; Value = value } 468 | | IntegerLiteralTypeConst -> 469 | let value = jobj.["value"].Value() 470 | Type.IntegerLiteralType { Kind = kind; Value = value } 471 | | BooleanLiteralTypeConst -> 472 | let value = jobj.["value"].Value() 473 | Type.BooleanLiteralType { Kind = kind; Value = value } 474 | | _ -> failwithf "Unknown type kind: %s" kind 475 | 476 | 477 | let metaModelSerializerSettings = 478 | let settings = JsonSerializerSettings() 479 | settings.Converters.Add(Converters.TypeConverter() :> JsonConverter) 480 | settings.Converters.Add(Converters.MapKeyTypeConverter() :> JsonConverter) 481 | settings.Converters.Add(JsonUtils.OptionConverter() :> JsonConverter) 482 | settings 483 | 484 | let isNullableType (t: MetaModel.Type) = 485 | match t with 486 | | MetaModel.Type.BaseType { Name = MetaModel.BaseTypes.Null } -> true 487 | | _ -> false -------------------------------------------------------------------------------- /tools/MetaModelGenerator/MetaModelGenerator.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tools/MetaModelGenerator/PrimitiveExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace MetaModelGenerator 2 | 3 | module Option = 4 | module Array = 5 | /// Returns true if the given array is empty or None 6 | let isEmpty (x: 'a array option) = 7 | match x with 8 | | None -> true 9 | | Some x -> Array.isEmpty x 10 | 11 | /// Returns empty array if None, otherwise the array 12 | let toArray (x: 'a array option) = Option.defaultValue [||] x 13 | 14 | let mapOrDefault def f o = 15 | match o with 16 | | Some o -> f o 17 | | None -> def 18 | 19 | module String = 20 | open System 21 | 22 | let toPascalCase (s: string) = 23 | s.[0] 24 | |> Char.ToUpper 25 | |> fun c -> 26 | c.ToString() 27 | + s.Substring(1) 28 | 29 | module Array = 30 | /// Places separator between each element of items 31 | let intersperse (separator: 'a) (items: 'a array) : 'a array = [| 32 | let mutable notFirst = false 33 | 34 | for element in items do 35 | if notFirst then 36 | yield separator 37 | 38 | yield element 39 | notFirst <- true 40 | |] -------------------------------------------------------------------------------- /tools/MetaModelGenerator/Program.fs: -------------------------------------------------------------------------------- 1 | namespace MetaModelGenerator 2 | 3 | module Main = 4 | open Argu 5 | open System 6 | open Newtonsoft.Json 7 | open System.IO 8 | 9 | type TypeArgs = 10 | | MetaModelPath of string 11 | | OutputFilePath of string 12 | 13 | interface IArgParserTemplate with 14 | member this.Usage: string = 15 | match this with 16 | | MetaModelPath _ -> 17 | "The path to metaModel.json. See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#metaModel" 18 | | OutputFilePath _ -> "The path to the output file. Should end with .fs" 19 | 20 | type ClientServerArgs = 21 | | MetaModelPath of string 22 | | OutputFilePath of string 23 | 24 | interface IArgParserTemplate with 25 | member this.Usage: string = 26 | match this with 27 | | MetaModelPath _ -> 28 | "The path to metaModel.json. See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#metaModel" 29 | | OutputFilePath _ -> "The path to the output file. Should end with .fs" 30 | 31 | type CommandArgs = 32 | | [] Types of ParseResults 33 | | [] ClientServer of ParseResults 34 | 35 | interface IArgParserTemplate with 36 | member this.Usage = 37 | match this with 38 | | Types _ -> "Generates Types from metaModel.json." 39 | | ClientServer _ -> "Generates Client/Server" 40 | 41 | let readMetaModel metamodelPath = 42 | async { 43 | 44 | printfn "Reading in %s" metamodelPath 45 | 46 | let! metaModel = 47 | File.ReadAllTextAsync(metamodelPath) 48 | |> Async.AwaitTask 49 | 50 | printfn "Deserializing metaModel" 51 | 52 | let parsedMetaModel = 53 | JsonConvert.DeserializeObject(metaModel, MetaModel.metaModelSerializerSettings) 54 | 55 | return parsedMetaModel 56 | } 57 | 58 | 59 | [] 60 | let main argv = 61 | 62 | let errorHandler = 63 | ProcessExiter( 64 | colorizer = 65 | function 66 | | ErrorCode.HelpText -> None 67 | | _ -> Some ConsoleColor.Red 68 | ) 69 | 70 | let parser = ArgumentParser.Create(programName = "MetaModelGenerator", errorHandler = errorHandler) 71 | 72 | let results = parser.ParseCommandLine argv 73 | 74 | match results.GetSubCommand() with 75 | | Types r -> 76 | let metaModelPath = r.GetResult <@ TypeArgs.MetaModelPath @> 77 | let OutputFilePath = r.GetResult <@ TypeArgs.OutputFilePath @> 78 | 79 | let metaModel = 80 | readMetaModel metaModelPath 81 | |> Async.RunSynchronously 82 | 83 | GenerateTypes.generateType metaModel OutputFilePath 84 | |> Async.RunSynchronously 85 | 86 | | ClientServer r -> 87 | 88 | let metaModelPath = r.GetResult <@ ClientServerArgs.MetaModelPath @> 89 | let OutputFilePath = r.GetResult <@ ClientServerArgs.OutputFilePath @> 90 | 91 | let metaModel = 92 | readMetaModel metaModelPath 93 | |> Async.RunSynchronously 94 | 95 | GenerateClientServer.generateClientServer metaModel OutputFilePath 96 | |> Async.RunSynchronously 97 | 98 | 0 --------------------------------------------------------------------------------