Repository: openlawlibrary/pygls
Pull request #380 (2023-09-26 23:58:24)

Add more capability unit tests and enable coverage reporting
## Description (e.g. "Related to ...", etc.)

This PR attempts to address issues around computing server capabilities raised in #377 

- With these changes I'm now able to run the jedi-language-server test suite on my machine
- Thanks to the type annotation suggestions from @dimbleby, the `_provider_options` method is now annotated, allowing mypy to check it's usage.
- The `default` argument to `_provide_options` has been made mandatory. 

Also 
- I've also made an attempt at enabling test coverage - though I am not 100% sure if reporting of [subprocess](https://coverage.readthedocs.io/en/7.2.7/subprocess.html) is working or not! :sweat_smile: 
- `poe` will now always run all lints, even when some of them fail so that it's possible to see all failures at once.

## Code review checklist (for code reviewer to complete)

- [ ] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR)
- [ ] Title summarizes what is changing
- [ ] Commit messages are meaningful (see [this][commit messages] for details)
- [ ] Tests have been included and/or updated, as appropriate
- [ ] Docstrings have been included and/or updated, as appropriate
- [ ] Standalone docs have been updated accordingly

[commit messages]: https://conventionalcommits.org/


dimbleby: (I don't care either way about the import changes: except that that putting such things into the same commit as the actual substantial change that you are making is unhelpful for reviewers!)

I'd suggest that the default value for `_provider_options()` should be `None` rather than `True`: and that callers who want a `True` default should have to say so.  It sort of doesn't matter, you will feel free to ignore me, clearly it's possible to write correct code either way.  But I reckon that this is what allowed the confusion that caused the bug to be written in the first place: `None` is the conventional default on lots of APIs and it's natural for developers to expectt the same here.
alcarney: > (I don't care either way about the import changes: except that that putting such things into the same commit as the actual substantial change that you are making is unhelpful for reviewers!)

Apologies! I probably should've marked this as draft before, as I wasn't necessarily expecting a review :sweat_smile: 
tombh: I approved the PR, but @dimbleby, please chime in as well if anything stands out.

Coverage is a nice touch! Thoughts for another time:
Should set it up so that a PR is refused if coverage goes down? We could add https://coveralls.io integration for nice graphs and stuff?
alcarney: > Should we set it up so that a PR is refused if coverage goes down? We could add https://coveralls.io/ integration for nice graphs and stuff? 

Up to you. 

Another option is to copy the attrs project and have the report show up in the [workflow summary](https://github.com/python-attrs/attrs/actions/runs/6348188934). It looks like it would be [fairly easy](https://github.com/python-attrs/attrs/blob/1046c75480bea76551ed7f976acfebc90c9c0b4e/.github/workflows/ci.yml#L102) to setup 

Patch:
diff --git a/poetry.lock b/poetry.lock
index e939750..3499ed9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -325,6 +325,81 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "coverage"
+version = "7.2.7"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+    {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+    {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+    {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+    {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+    {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+    {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+    {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+    {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+    {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+    {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+    {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+    {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+    {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+    {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+    {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+    {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+    {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+    {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+    {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+    {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+    {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+    {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+    {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
 [[package]]
 name = "docutils"
 version = "0.18.1"
@@ -1285,4 +1360,4 @@ ws = ["websockets"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.7.9,<4"
-content-hash = "36d67b26c8878f526a5d76cf5f7eb8173f55b35bec57b164972a8b8ea1d98b8c"
+content-hash = "ea468ceb5e9f165f022c3fad800f9d063f04fa8034b4f07ff96921b86940afe0"
diff --git a/pygls/capabilities.py b/pygls/capabilities.py
index 852b5cf..9db4744 100644
--- a/pygls/capabilities.py
+++ b/pygls/capabilities.py
@@ -15,84 +15,18 @@
 # limitations under the License.                                           #
 ############################################################################
 from functools import reduce
-from typing import Any, Dict, List, Optional, Set, Union
+from typing import Any, Dict, List, Optional, Set, Union, TypeVar
 import logging
 
-from lsprotocol.types import (
-    INLAY_HINT_RESOLVE,
-    TEXT_DOCUMENT_CODE_ACTION,
-    TEXT_DOCUMENT_CODE_LENS,
-    TEXT_DOCUMENT_COMPLETION,
-    TEXT_DOCUMENT_DECLARATION,
-    TEXT_DOCUMENT_DEFINITION,
-    TEXT_DOCUMENT_DIAGNOSTIC,
-    TEXT_DOCUMENT_DOCUMENT_COLOR,
-    TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT,
-    TEXT_DOCUMENT_DOCUMENT_LINK,
-    TEXT_DOCUMENT_DOCUMENT_SYMBOL,
-    TEXT_DOCUMENT_FOLDING_RANGE,
-    TEXT_DOCUMENT_FORMATTING,
-    TEXT_DOCUMENT_HOVER,
-    TEXT_DOCUMENT_IMPLEMENTATION,
-    TEXT_DOCUMENT_INLAY_HINT,
-    TEXT_DOCUMENT_INLINE_VALUE,
-    TEXT_DOCUMENT_ON_TYPE_FORMATTING,
-    TEXT_DOCUMENT_RANGE_FORMATTING,
-    TEXT_DOCUMENT_REFERENCES,
-    TEXT_DOCUMENT_RENAME,
-    TEXT_DOCUMENT_SELECTION_RANGE,
-    TEXT_DOCUMENT_SIGNATURE_HELP,
-    TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY,
-    TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY,
-    TEXT_DOCUMENT_DID_CLOSE,
-    TEXT_DOCUMENT_DID_OPEN,
-    TEXT_DOCUMENT_DID_SAVE,
-    TEXT_DOCUMENT_LINKED_EDITING_RANGE,
-    TEXT_DOCUMENT_MONIKER,
-    TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
-    TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA,
-    TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE,
-    TEXT_DOCUMENT_WILL_SAVE,
-    TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
-    TEXT_DOCUMENT_TYPE_DEFINITION,
-    WORKSPACE_DIAGNOSTIC,
-    WORKSPACE_DID_CREATE_FILES,
-    WORKSPACE_DID_DELETE_FILES,
-    WORKSPACE_DID_RENAME_FILES,
-    WORKSPACE_SYMBOL,
-    WORKSPACE_SYMBOL_RESOLVE,
-    WORKSPACE_WILL_CREATE_FILES,
-    WORKSPACE_WILL_DELETE_FILES,
-    WORKSPACE_WILL_RENAME_FILES,
-    InlayHintOptions,
-    PositionEncodingKind,
-)
-from lsprotocol.types import (
-    ClientCapabilities,
-    CodeLensOptions,
-    CompletionOptions,
-    DocumentLinkOptions,
-    ExecuteCommandOptions,
-    ImplementationOptions,
-    NotebookDocumentSyncOptions,
-    SemanticTokensOptions,
-    SemanticTokensRegistrationOptions,
-    SemanticTokensOptionsFullType1,
-    ServerCapabilities,
-    ServerCapabilitiesWorkspaceType,
-    SignatureHelpOptions,
-    TextDocumentSyncKind,
-    TextDocumentSyncOptions,
-    TypeDefinitionOptions,
-    FileOperationOptions,
-    WorkspaceFoldersServerCapabilities,
-)
+from lsprotocol import types
+
 
 logger = logging.getLogger(__name__)
+T = TypeVar("T")
 
 
 def get_capability(
-    client_capabilities: ClientCapabilities, field: str, default: Any = None
+    client_capabilities: types.ClientCapabilities, field: str, default: Any = None
 ) -> Any:
     """Check if ClientCapabilities has some nested value without raising
     AttributeError.
@@ -114,12 +48,12 @@ class ServerCapabilitiesBuilder:
 
     def __init__(
         self,
-        client_capabilities: ClientCapabilities,
+        client_capabilities: types.ClientCapabilities,
         features: Set[str],
         feature_options: Dict[str, Any],
         commands: List[str],
-        text_document_sync_kind: TextDocumentSyncKind,
-        notebook_document_sync: Optional[NotebookDocumentSyncOptions] = None,
+        text_document_sync_kind: types.TextDocumentSyncKind,
+        notebook_document_sync: Optional[types.NotebookDocumentSyncOptions] = None,
     ):
         self.client_capabilities = client_capabilities
         self.features = features
@@ -128,37 +62,37 @@ class ServerCapabilitiesBuilder:
         self.text_document_sync_kind = text_document_sync_kind
         self.notebook_document_sync = notebook_document_sync
 
-        self.server_cap = ServerCapabilities()
+        self.server_cap = types.ServerCapabilities()
 
-    def _provider_options(self, feature, default=True):
+    def _provider_options(self, feature: str, default: T) -> Optional[Union[T, Any]]:
         if feature in self.features:
             return self.feature_options.get(feature, default)
         return None
 
     def _with_text_document_sync(self):
         open_close = (
-            TEXT_DOCUMENT_DID_OPEN in self.features
-            or TEXT_DOCUMENT_DID_CLOSE in self.features
+            types.TEXT_DOCUMENT_DID_OPEN in self.features
+            or types.TEXT_DOCUMENT_DID_CLOSE in self.features
         )
         will_save = (
             get_capability(
                 self.client_capabilities, "text_document.synchronization.will_save"
             )
-            and TEXT_DOCUMENT_WILL_SAVE in self.features
+            and types.TEXT_DOCUMENT_WILL_SAVE in self.features
         )
         will_save_wait_until = (
             get_capability(
                 self.client_capabilities,
                 "text_document.synchronization.will_save_wait_until",
             )
-            and TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL in self.features
+            and types.TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL in self.features
         )
-        if TEXT_DOCUMENT_DID_SAVE in self.features:
-            save = self.feature_options.get(TEXT_DOCUMENT_DID_SAVE, True)
+        if types.TEXT_DOCUMENT_DID_SAVE in self.features:
+            save = self.feature_options.get(types.TEXT_DOCUMENT_DID_SAVE, True)
         else:
             save = False
 
-        self.server_cap.text_document_sync = TextDocumentSyncOptions(
+        self.server_cap.text_document_sync = types.TextDocumentSyncOptions(
             open_close=open_close,
             change=self.text_document_sync_kind,
             will_save=will_save,
@@ -177,41 +111,41 @@ class ServerCapabilitiesBuilder:
 
     def _with_completion(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_COMPLETION, default=CompletionOptions()
+            types.TEXT_DOCUMENT_COMPLETION, default=types.CompletionOptions()
         )
         if value is not None:
             self.server_cap.completion_provider = value
         return self
 
     def _with_hover(self):
-        value = self._provider_options(TEXT_DOCUMENT_HOVER)
+        value = self._provider_options(types.TEXT_DOCUMENT_HOVER, default=True)
         if value is not None:
             self.server_cap.hover_provider = value
         return self
 
     def _with_signature_help(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_SIGNATURE_HELP, default=SignatureHelpOptions()
+            types.TEXT_DOCUMENT_SIGNATURE_HELP, default=types.SignatureHelpOptions()
         )
         if value is not None:
             self.server_cap.signature_help_provider = value
         return self
 
     def _with_declaration(self):
-        value = self._provider_options(TEXT_DOCUMENT_DECLARATION)
+        value = self._provider_options(types.TEXT_DOCUMENT_DECLARATION, default=True)
         if value is not None:
             self.server_cap.declaration_provider = value
         return self
 
     def _with_definition(self):
-        value = self._provider_options(TEXT_DOCUMENT_DEFINITION)
+        value = self._provider_options(types.TEXT_DOCUMENT_DEFINITION, default=True)
         if value is not None:
             self.server_cap.definition_provider = value
         return self
 
     def _with_type_definition(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_TYPE_DEFINITION, default=TypeDefinitionOptions()
+            types.TEXT_DOCUMENT_TYPE_DEFINITION, default=types.TypeDefinitionOptions()
         )
         if value is not None:
             self.server_cap.type_definition_provider = value
@@ -219,48 +153,52 @@ class ServerCapabilitiesBuilder:
 
     def _with_inlay_hints(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_INLAY_HINT, default=InlayHintOptions()
+            types.TEXT_DOCUMENT_INLAY_HINT, default=types.InlayHintOptions()
         )
         if value is not None:
-            value.resolve_provider = INLAY_HINT_RESOLVE in self.features
+            value.resolve_provider = types.INLAY_HINT_RESOLVE in self.features
             self.server_cap.inlay_hint_provider = value
         return self
 
     def _with_implementation(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_IMPLEMENTATION, default=ImplementationOptions()
+            types.TEXT_DOCUMENT_IMPLEMENTATION, default=types.ImplementationOptions()
         )
         if value is not None:
             self.server_cap.implementation_provider = value
         return self
 
     def _with_references(self):
-        value = self._provider_options(TEXT_DOCUMENT_REFERENCES)
+        value = self._provider_options(types.TEXT_DOCUMENT_REFERENCES, default=True)
         if value is not None:
             self.server_cap.references_provider = value
         return self
 
     def _with_document_highlight(self):
-        value = self._provider_options(TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT, default=True
+        )
         if value is not None:
             self.server_cap.document_highlight_provider = value
         return self
 
     def _with_document_symbol(self):
-        value = self._provider_options(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_DOCUMENT_SYMBOL, default=True
+        )
         if value is not None:
             self.server_cap.document_symbol_provider = value
         return self
 
     def _with_code_action(self):
-        value = self._provider_options(TEXT_DOCUMENT_CODE_ACTION)
+        value = self._provider_options(types.TEXT_DOCUMENT_CODE_ACTION, default=True)
         if value is not None:
             self.server_cap.code_action_provider = value
         return self
 
     def _with_code_lens(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_CODE_LENS, default=CodeLensOptions()
+            types.TEXT_DOCUMENT_CODE_LENS, default=types.CodeLensOptions()
         )
         if value is not None:
             self.server_cap.code_lens_provider = value
@@ -268,102 +206,113 @@ class ServerCapabilitiesBuilder:
 
     def _with_document_link(self):
         value = self._provider_options(
-            TEXT_DOCUMENT_DOCUMENT_LINK, default=DocumentLinkOptions()
+            types.TEXT_DOCUMENT_DOCUMENT_LINK, default=types.DocumentLinkOptions()
         )
         if value is not None:
             self.server_cap.document_link_provider = value
         return self
 
     def _with_color(self):
-        value = self._provider_options(TEXT_DOCUMENT_DOCUMENT_COLOR)
+        value = self._provider_options(types.TEXT_DOCUMENT_DOCUMENT_COLOR, default=True)
         if value is not None:
             self.server_cap.color_provider = value
         return self
 
     def _with_document_formatting(self):
-        value = self._provider_options(TEXT_DOCUMENT_FORMATTING)
+        value = self._provider_options(types.TEXT_DOCUMENT_FORMATTING, default=True)
         if value is not None:
             self.server_cap.document_formatting_provider = value
         return self
 
     def _with_document_range_formatting(self):
-        value = self._provider_options(TEXT_DOCUMENT_RANGE_FORMATTING)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_RANGE_FORMATTING, default=True
+        )
         if value is not None:
             self.server_cap.document_range_formatting_provider = value
         return self
 
     def _with_document_on_type_formatting(self):
-        value = self._provider_options(TEXT_DOCUMENT_ON_TYPE_FORMATTING)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_ON_TYPE_FORMATTING, default=None
+        )
         if value is not None:
             self.server_cap.document_on_type_formatting_provider = value
         return self
 
     def _with_rename(self):
-        value = self._provider_options(TEXT_DOCUMENT_RENAME)
+        value = self._provider_options(types.TEXT_DOCUMENT_RENAME, default=True)
         if value is not None:
             self.server_cap.rename_provider = value
         return self
 
     def _with_folding_range(self):
-        value = self._provider_options(TEXT_DOCUMENT_FOLDING_RANGE)
+        value = self._provider_options(types.TEXT_DOCUMENT_FOLDING_RANGE, default=True)
         if value is not None:
             self.server_cap.folding_range_provider = value
         return self
 
     def _with_execute_command(self):
-        self.server_cap.execute_command_provider = ExecuteCommandOptions(
+        self.server_cap.execute_command_provider = types.ExecuteCommandOptions(
             commands=self.commands
         )
         return self
 
     def _with_selection_range(self):
-        value = self._provider_options(TEXT_DOCUMENT_SELECTION_RANGE)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_SELECTION_RANGE, default=True
+        )
         if value is not None:
             self.server_cap.selection_range_provider = value
         return self
 
     def _with_call_hierarchy(self):
-        value = self._provider_options(TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY, default=True
+        )
         if value is not None:
             self.server_cap.call_hierarchy_provider = value
         return self
 
     def _with_type_hierarchy(self):
-        value = self._provider_options(TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY, default=True
+        )
         if value is not None:
             self.server_cap.type_hierarchy_provider = value
         return self
 
     def _with_semantic_tokens(self):
         providers = [
-            TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
-            TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA,
-            TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE,
+            types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
+            types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA,
+            types.TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE,
         ]
 
+        value = None
         for provider in providers:
-            value = self._provider_options(provider, None)
-            if value:
+            value = self._provider_options(provider, default=None)
+            if value is not None:
                 break
 
         if value is None:
             return self
 
-        if isinstance(value, SemanticTokensRegistrationOptions):
+        if isinstance(value, types.SemanticTokensRegistrationOptions):
             self.server_cap.semantic_tokens_provider = value
             return self
 
-        full_support: Union[bool, SemanticTokensOptionsFullType1] = (
-            TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL in self.features
+        full_support: Union[bool, types.SemanticTokensOptionsFullType1] = (
+            types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL in self.features
         )
 
-        if TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA in self.features:
-            full_support = SemanticTokensOptionsFullType1(delta=True)
+        if types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA in self.features:
+            full_support = types.SemanticTokensOptionsFullType1(delta=True)
 
-        options = SemanticTokensOptions(
+        options = types.SemanticTokensOptions(
             legend=value,
             full=full_support or None,
-            range=TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE in self.features or None,
+            range=types.TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE in self.features or None,
         )
 
         if options.full or options.range:
@@ -372,34 +321,38 @@ class ServerCapabilitiesBuilder:
         return self
 
     def _with_linked_editing_range(self):
-        value = self._provider_options(TEXT_DOCUMENT_LINKED_EDITING_RANGE)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_LINKED_EDITING_RANGE, default=True
+        )
         if value is not None:
             self.server_cap.linked_editing_range_provider = value
         return self
 
     def _with_moniker(self):
-        value = self._provider_options(TEXT_DOCUMENT_MONIKER)
+        value = self._provider_options(types.TEXT_DOCUMENT_MONIKER, default=True)
         if value is not None:
             self.server_cap.moniker_provider = value
         return self
 
     def _with_workspace_symbol(self):
-        value = self._provider_options(WORKSPACE_SYMBOL)
+        value = self._provider_options(
+            types.WORKSPACE_SYMBOL, default=types.WorkspaceSymbolOptions()
+        )
         if value is not None:
-            value.resolve_provider = self._provider_options(WORKSPACE_SYMBOL_RESOLVE)
+            value.resolve_provider = types.WORKSPACE_SYMBOL_RESOLVE in self.features
             self.server_cap.workspace_symbol_provider = value
         return self
 
     def _with_workspace_capabilities(self):
         # File operations
-        file_operations = FileOperationOptions()
+        file_operations = types.FileOperationOptions()
         operations = [
-            (WORKSPACE_WILL_CREATE_FILES, "will_create"),
-            (WORKSPACE_DID_CREATE_FILES, "did_create"),
-            (WORKSPACE_WILL_DELETE_FILES, "will_delete"),
-            (WORKSPACE_DID_DELETE_FILES, "did_delete"),
-            (WORKSPACE_WILL_RENAME_FILES, "will_rename"),
-            (WORKSPACE_DID_RENAME_FILES, "did_rename"),
+            (types.WORKSPACE_WILL_CREATE_FILES, "will_create"),
+            (types.WORKSPACE_DID_CREATE_FILES, "did_create"),
+            (types.WORKSPACE_WILL_DELETE_FILES, "will_delete"),
+            (types.WORKSPACE_DID_DELETE_FILES, "did_delete"),
+            (types.WORKSPACE_WILL_RENAME_FILES, "will_rename"),
+            (types.WORKSPACE_DID_RENAME_FILES, "did_rename"),
         ]
 
         for method_name, capability_name in operations:
@@ -408,11 +361,11 @@ class ServerCapabilitiesBuilder:
             )
 
             if client_supports_method:
-                value = self._provider_options(method_name, None)
+                value = self._provider_options(method_name, default=None)
                 setattr(file_operations, capability_name, value)
 
-        self.server_cap.workspace = ServerCapabilitiesWorkspaceType(
-            workspace_folders=WorkspaceFoldersServerCapabilities(
+        self.server_cap.workspace = types.ServerCapabilitiesWorkspaceType(
+            workspace_folders=types.WorkspaceFoldersServerCapabilities(
                 supported=True,
                 change_notifications=True,
             ),
@@ -421,20 +374,25 @@ class ServerCapabilitiesBuilder:
         return self
 
     def _with_diagnostic_provider(self):
-        value = self._provider_options(TEXT_DOCUMENT_DIAGNOSTIC)
+        value = self._provider_options(
+            types.TEXT_DOCUMENT_DIAGNOSTIC,
+            default=types.DiagnosticOptions(
+                inter_file_dependencies=False, workspace_diagnostics=False
+            ),
+        )
         if value is not None:
-            value.workspace_diagnostics = self._provider_options(WORKSPACE_DIAGNOSTIC)
+            value.workspace_diagnostics = types.WORKSPACE_DIAGNOSTIC in self.features
             self.server_cap.diagnostic_provider = value
         return self
 
     def _with_inline_value_provider(self):
-        value = self._provider_options(TEXT_DOCUMENT_INLINE_VALUE)
+        value = self._provider_options(types.TEXT_DOCUMENT_INLINE_VALUE, default=True)
         if value is not None:
             self.server_cap.inline_value_provider = value
         return self
 
     def _with_position_encodings(self):
-        self.server_cap.position_encoding = PositionEncodingKind.Utf16
+        self.server_cap.position_encoding = types.PositionEncodingKind.Utf16
 
         general = self.client_capabilities.general
         if general is None:
@@ -444,15 +402,15 @@ class ServerCapabilitiesBuilder:
         if encodings is None:
             return self
 
-        if PositionEncodingKind.Utf16 in encodings:
+        if types.PositionEncodingKind.Utf16 in encodings:
             return self
 
-        if PositionEncodingKind.Utf32 in encodings:
-            self.server_cap.position_encoding = PositionEncodingKind.Utf32
+        if types.PositionEncodingKind.Utf32 in encodings:
+            self.server_cap.position_encoding = types.PositionEncodingKind.Utf32
             return self
 
-        if PositionEncodingKind.Utf8 in encodings:
-            self.server_cap.position_encoding = PositionEncodingKind.Utf8
+        if types.PositionEncodingKind.Utf8 in encodings:
+            self.server_cap.position_encoding = types.PositionEncodingKind.Utf8
             return self
 
         logger.warning(f"Unknown `PositionEncoding`s: {encodings}")
diff --git a/pyproject.toml b/pyproject.toml
index a1eb850..931fa29 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ mypy = "^1.4.1"
 black = "22.8"
 
 [tool.poetry.group.test.dependencies]
+coverage = { version = "^7.2.1", extras = ["toml"] }
 pytest = "7.1.2"
 pytest-asyncio = "^0.21.0"
 
@@ -47,22 +48,35 @@ selenium = "^4.10.0"
 asyncio_mode = "auto"
 
 [tool.poe.tasks]
-test = "pytest"
 test-pyodide = "python tests/pyodide_testrunner/run.py"
 ruff = "ruff check ."
 mypy = "mypy -p pygls"
 check_generated_client = "python scripts/check_client_is_uptodate.py"
 check_commit_style = "npx commitlint --from origin/main --to HEAD --verbose --config commitlintrc.yaml"
-lint = [
+generate_client = "python scripts/generate_client.py --output pygls/lsp/client.py"
+generate_contributors_md = "python scripts/generate_contributors_md.py"
+black_check = "black --check ."
+
+[tool.poe.tasks.test]
+env = { COVERAGE_PROCESS_START = "pyproject.toml" }
+sequence = [
+     { cmd = "python -c 'import pathlib,sys; pathlib.Path(f\"{sys.path[-1]}/cov.pth\").write_text(\"import coverage; coverage.process_startup()\");'"},
+     { cmd = "coverage erase" },
+     { cmd = "coverage run -m pytest" },
+     { cmd = "coverage combine" },
+     { cmd = "coverage report" },
+]
+ignore_fail = "return_non_zero"
+
+[tool.poe.tasks.lint]
+sequence = [
     "ruff",
     "mypy",
     "check_generated_client",
     "check_commit_style",
     "black_check"
 ]
-generate_client = "python scripts/generate_client.py --output pygls/lsp/client.py"
-generate_contributors_md = "python scripts/generate_contributors_md.py"
-black_check = "black --check ."
+ignore_fail = "return_non_zero"
 
 [tool.pyright]
 strict = ["pygls"]
@@ -76,6 +90,15 @@ line-length = 120
 line-length = 88
 extend-exclude = "pygls/lsp/client.py"
 
+[tool.coverage.run]
+parallel = true
+source_pkgs = ["pygls"]
+
+[tool.coverage.report]
+show_missing = true
+skip_covered = true
+sort = "Cover"
+
 [tool.mypy]
 check_untyped_defs = true
 


Test patch:
diff --git a/tests/test_feature_manager.py b/tests/test_feature_manager.py
index 4e5c852..9d299e8 100644
--- a/tests/test_feature_manager.py
+++ b/tests/test_feature_manager.py
@@ -601,6 +601,59 @@ def server_capabilities(**kwargs):
                 )
             ),
         ),
+        (
+            lsp.WORKSPACE_SYMBOL,
+            None,
+            lsp.ClientCapabilities(),
+            server_capabilities(
+                workspace_symbol_provider=lsp.WorkspaceSymbolOptions(
+                    resolve_provider=False,
+                ),
+            ),
+        ),
+        (
+            lsp.TEXT_DOCUMENT_DIAGNOSTIC,
+            None,
+            lsp.ClientCapabilities(),
+            server_capabilities(
+                diagnostic_provider=lsp.DiagnosticOptions(
+                    inter_file_dependencies=False,
+                    workspace_diagnostics=False,
+                ),
+            ),
+        ),
+        (
+            lsp.TEXT_DOCUMENT_DIAGNOSTIC,
+            lsp.DiagnosticOptions(
+                workspace_diagnostics=True,
+                inter_file_dependencies=True,
+            ),
+            lsp.ClientCapabilities(),
+            server_capabilities(
+                diagnostic_provider=lsp.DiagnosticOptions(
+                    inter_file_dependencies=True,
+                    workspace_diagnostics=False,
+                ),
+            ),
+        ),
+        (
+            lsp.TEXT_DOCUMENT_ON_TYPE_FORMATTING,
+            None,
+            lsp.ClientCapabilities(),
+            server_capabilities(
+                document_on_type_formatting_provider=None,
+            ),
+        ),
+        (
+            lsp.TEXT_DOCUMENT_ON_TYPE_FORMATTING,
+            lsp.DocumentOnTypeFormattingOptions(first_trigger_character=":"),
+            lsp.ClientCapabilities(),
+            server_capabilities(
+                document_on_type_formatting_provider=lsp.DocumentOnTypeFormattingOptions(
+                    first_trigger_character=":",
+                ),
+            ),
+        ),
     ],
 )
 def test_register_feature(
@@ -648,9 +701,7 @@ def test_register_feature(
     assert expected == actual
 
 
-def test_register_inlay_hint_resolve(
-    feature_manager: FeatureManager,
-):
+def test_register_inlay_hint_resolve(feature_manager: FeatureManager):
     @feature_manager.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
     def _():
         pass
@@ -673,3 +724,64 @@ def test_register_inlay_hint_resolve(
     ).build()
 
     assert expected == actual
+
+
+def test_register_workspace_symbol_resolve(feature_manager: FeatureManager):
+    @feature_manager.feature(lsp.WORKSPACE_SYMBOL)
+    def _():
+        pass
+
+    @feature_manager.feature(lsp.WORKSPACE_SYMBOL_RESOLVE)
+    def _():
+        pass
+
+    expected = server_capabilities(
+        workspace_symbol_provider=lsp.WorkspaceSymbolOptions(resolve_provider=True),
+    )
+
+    actual = ServerCapabilitiesBuilder(
+        lsp.ClientCapabilities(),
+        feature_manager.features.keys(),
+        feature_manager.feature_options,
+        [],
+        None,
+        None,
+    ).build()
+
+    assert expected == actual
+
+
+def test_register_workspace_diagnostics(feature_manager: FeatureManager):
+    @feature_manager.feature(
+        lsp.TEXT_DOCUMENT_DIAGNOSTIC,
+        lsp.DiagnosticOptions(
+            identifier="example",
+            inter_file_dependencies=False,
+            workspace_diagnostics=False,
+        ),
+    )
+    def _():
+        pass
+
+    @feature_manager.feature(lsp.WORKSPACE_DIAGNOSTIC)
+    def _():
+        pass
+
+    expected = server_capabilities(
+        diagnostic_provider=lsp.DiagnosticOptions(
+            identifier="example",
+            inter_file_dependencies=False,
+            workspace_diagnostics=True,
+        ),
+    )
+
+    actual = ServerCapabilitiesBuilder(
+        lsp.ClientCapabilities(),
+        feature_manager.features.keys(),
+        feature_manager.feature_options,
+        [],
+        None,
+        None,
+    ).build()
+
+    assert expected == actual