├── .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 |
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 |
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
--------------------------------------------------------------------------------