diff options
Diffstat (limited to 'docs/source/pages')
-rw-r--r-- | docs/source/pages/getting_started.rst | 94 | ||||
-rw-r--r-- | docs/source/pages/migrating-to-v1.rst | 241 | ||||
-rw-r--r-- | docs/source/pages/reference.rst | 8 | ||||
-rw-r--r-- | docs/source/pages/reference/clients.rst | 8 | ||||
-rw-r--r-- | docs/source/pages/reference/protocol.rst | 11 | ||||
-rw-r--r-- | docs/source/pages/reference/servers.rst | 11 | ||||
-rw-r--r-- | docs/source/pages/reference/types.rst | 10 | ||||
-rw-r--r-- | docs/source/pages/reference/workspace.rst | 9 | ||||
-rw-r--r-- | docs/source/pages/testing.rst | 24 | ||||
-rw-r--r-- | docs/source/pages/tutorial.rst | 207 | ||||
-rw-r--r-- | docs/source/pages/user-guide.rst | 536 |
11 files changed, 1159 insertions, 0 deletions
diff --git a/docs/source/pages/getting_started.rst b/docs/source/pages/getting_started.rst new file mode 100644 index 0000000..e73d20f --- /dev/null +++ b/docs/source/pages/getting_started.rst @@ -0,0 +1,94 @@ +Getting Started +=============== + +This document explains how to install *pygls* and get started writing language +servers that are based on it. + +.. note:: + + Before going any further, if you are not familiar with *language servers* + and *Language Server Protocol*, we recommend reading following articles: + + - `Language Server Protocol Overview <https://microsoft.github.io/language-server-protocol/overview>`_ + - `Language Server Protocol Specification <https://microsoft.github.io/language-server-protocol/specification>`_ + - `Language Server Protocol SDKs <https://microsoft.github.io/language-server-protocol/implementors/sdks/>`_ + + +Installation +------------ + +To get the latest release from *PyPI*, simply run: + +.. code:: console + + pip install pygls + +Alternatively, *pygls* source code can be downloaded from our `GitHub`_ page and installed with following command: + +.. code:: console + + pip install git+https://github.com/openlawlibrary/pygls + +Quick Start +----------- + +Spin the Server Up +~~~~~~~~~~~~~~~~~~ + +*pygls* is a language server that can be started without writing any additional +code: + +.. code:: python + + from pygls.server import LanguageServer + + server = LanguageServer('example-server', 'v0.1') + + server.start_tcp('127.0.0.1', 8080) + +After running the code above, server will start listening for incoming +``Json RPC`` requests on ``http://127.0.0.1:8080``. + +Register Features and Commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*pygls* comes with an API for registering additional features like +``code completion``, ``find all references``, ``go to definition``, etc. + +.. code:: python + + @server.feature(TEXT_DOCUMENT_COMPLETION, CompletionOptions(trigger_characters=[','])) + def completions(params: CompletionParams): + """Returns completion items.""" + return CompletionList( + is_incomplete=False, + items=[ + CompletionItem(label='Item1'), + CompletionItem(label='Item2'), + CompletionItem(label='Item3'), + ] + ) + +… as well as custom commands: + +.. code:: python + + @server.command('myVerySpecialCommandName') + def cmd_return_hello_world(ls, *args): + return 'Hello World!' + +See the :mod:`lsprotocol.types` module for the complete and canonical list of available features. + +Tutorial +-------- + +We recommend completing the :ref:`tutorial <tutorial>`, especially if you +haven't worked with language servers before. + +User Guide +---------- + +To reveal the full potential of *pygls* (``thread management``, ``coroutines``, +``multi-root workspace``, ``TCP/STDIO communication``, etc.) keep reading. + +.. _GitHub: https://github.com/openlawlibrary/pygls diff --git a/docs/source/pages/migrating-to-v1.rst b/docs/source/pages/migrating-to-v1.rst new file mode 100644 index 0000000..527a810 --- /dev/null +++ b/docs/source/pages/migrating-to-v1.rst @@ -0,0 +1,241 @@ +Migrating to v1.0 +================= + +The most notable change of the ``v1.0`` release of ``pygls`` is the removal of its hand written LSP type and method definitions in favour of relying on the types provided by the `lsprotocol`_ library which are automatically generated from the LSP specification. +As as side effect this has also meant the removal of `pydantic`_ as a dependency, since ``lsprotocol`` uses `attrs`_ and `cattrs`_ for serialisation and validation. + +This guide outlines how to adapt an existing server to the breaking changes introduced in this release. + +Known Migrations +---------------- +You may find insight and inspiration from these projects that have already successfully migrated to v1: + +* `jedi-language-server`_ +* `vscode-ruff`_ +* `esbonio`_ +* `yara-language-server`_ + +Updating Imports +---------------- + +The ``pygls.lsp.methods`` and ``pygls.lsp.types`` modules no longer exist. +Instead, all types and method names should now be imported from the ``lsprotocol.types`` module. + +Additionally, the following types and constants have been renamed. + +================================================================== ============== +pygls lsprotocol +================================================================== ============== +``CODE_ACTION`` ``TEXT_DOCUMENT_CODE_ACTION`` +``CODE_LENS`` ``TEXT_DOCUMENT_CODE_LENS`` +``COLOR_PRESENTATION`` ``TEXT_DOCUMENT_COLOR_PRESENTATION`` +``COMPLETION`` ``TEXT_DOCUMENT_COMPLETION`` +``DECLARATION`` ``TEXT_DOCUMENT_DECLARATION`` +``DEFINITION`` ``TEXT_DOCUMENT_DEFINITION`` +``DOCUMENT_COLOR`` ``TEXT_DOCUMENT_DOCUMENT_COLOR`` +``DOCUMENT_HIGHLIGHT`` ``TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT`` +``DOCUMENT_LINK`` ``TEXT_DOCUMENT_DOCUMENT_LINK`` +``DOCUMENT_SYMBOL`` ``TEXT_DOCUMENT_DOCUMENT_SYMBOL`` +``FOLDING_RANGE`` ``TEXT_DOCUMENT_FOLDING_RANGE`` +``FORMATTING`` ``TEXT_DOCUMENT_FORMATTING`` +``HOVER`` ``TEXT_DOCUMENT_HOVER`` +``IMPLEMENTATION`` ``TEXT_DOCUMENT_IMPLEMENTATION`` +``LOG_TRACE_NOTIFICATION`` ``LOG_TRACE`` +``ON_TYPE_FORMATTING`` ``TEXT_DOCUMENT_ON_TYPE_FORMATTING`` +``PREPARE_RENAME`` ``TEXT_DOCUMENT_PREPARE_RENAME`` +``PROGRESS_NOTIFICATION`` ``PROGRESS`` +``RANGE_FORMATTING`` ``TEXT_DOCUMENT_RANGE_FORMATTING`` +``REFERENCES`` ``TEXT_DOCUMENT_REFERENCES`` +``RENAME`` ``TEXT_DOCUMENT_RENAME`` +``SELECTION_RANGE`` ``TEXT_DOCUMENT_SELECTION_RANGE`` +``SET_TRACE_NOTIFICATION`` ``SET_TRACE`` +``SIGNATURE_HELP`` ``TEXT_DOCUMENT_SIGNATURE_HELP`` +``TEXT_DOCUMENT_CALL_HIERARCHY_INCOMING_CALLS`` ``CALL_HIERARCHY_INCOMING_CALLS`` +``TEXT_DOCUMENT_CALL_HIERARCHY_OUTGOING_CALLS`` ``CALL_HIERARCHY_OUTGOING_CALLS`` +``TEXT_DOCUMENT_CALL_HIERARCHY_PREPARE`` ``TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY`` +``TYPE_DEFINITION`` ``TEXT_DOCUMENT_TYPE_DEFINITION`` +``WORKSPACE_FOLDERS`` ``WORKSPACE_WORKSPACE_FOLDERS`` +``ApplyWorkspaceEditResponse`` ``ApplyWorkspaceEditResult`` +``ClientInfo`` ``InitializeParamsClientInfoType`` +``CodeActionDisabled`` ``CodeActionDisabledType`` +``CodeActionLiteralSupportActionKindClientCapabilities`` ``CodeActionClientCapabilitiesCodeActionLiteralSupportTypeCodeActionKindType`` +``CodeActionLiteralSupportClientCapabilities`` ``CodeActionClientCapabilitiesCodeActionLiteralSupportType`` +``CompletionItemClientCapabilities`` ``CompletionClientCapabilitiesCompletionItemType`` +``CompletionItemKindClientCapabilities`` ``CompletionClientCapabilitiesCompletionItemKindType`` +``CompletionTagSupportClientCapabilities`` ``CompletionClientCapabilitiesCompletionItemTypeTagSupportType`` +``DocumentSymbolCapabilitiesTagSupport`` ``DocumentSymbolClientCapabilitiesTagSupportType`` +``InsertTextModeSupportClientCapabilities`` ``CompletionClientCapabilitiesCompletionItemTypeInsertTextModeSupportType`` +``MarkedStringType`` ``MarkedString`` +``MarkedString`` ``MarkedString_Type1`` +``PrepareRename`` ``PrepareRenameResult_Type1`` +``PublishDiagnosticsTagSupportClientCapabilities`` ``PublishDiagnosticsClientCapabilitiesTagSupportType`` +``ResolveSupportClientCapabilities`` ``CodeActionClientCapabilitiesResolveSupportType`` +``SemanticTokensRequestsFull`` ``SemanticTokensRegistrationOptionsFullType1`` +``SemanticTokensRequests`` ``SemanticTokensClientCapabilitiesRequestsType`` +``ServerInfo`` ``InitializeResultServerInfoType`` +``ShowMessageRequestActionItem`` ``ShowMessageRequestClientCapabilitiesMessageActionItemType`` +``SignatureHelpInformationClientCapabilities`` ``SignatureHelpClientCapabilitiesSignatureInformationType`` +``SignatureHelpInformationParameterInformationClientCapabilities`` ``SignatureHelpClientCapabilitiesSignatureInformationTypeParameterInformationType`` +``TextDocumentContentChangeEvent`` ``TextDocumentContentChangeEvent_Type1`` +``TextDocumentContentChangeTextEvent`` ``TextDocumentContentChangeEvent_Type2`` +``TextDocumentSyncOptionsServerCapabilities`` ``TextDocumentSyncOptions`` +``Trace`` ``TraceValues`` +``URI`` ``str`` +``WorkspaceCapabilitiesSymbolKind`` ``WorkspaceSymbolClientCapabilitiesSymbolKindType`` +``WorkspaceCapabilitiesTagSupport`` ``WorkspaceSymbolClientCapabilitiesTagSupportType`` +``WorkspaceFileOperationsServerCapabilities`` ``FileOperationOptions`` +``WorkspaceServerCapabilities`` ``ServerCapabilitiesWorkspaceType`` +================================================================== ============== + +Custom Models +------------- + +One of the most obvious changes is the switch to `attrs`_ and `cattrs`_ for serialization and deserialisation. +This means that any custom models used by your language server will need to be converted to an ``attrs`` style class. + +.. code-block:: python + + # Before + from pydantic import BaseModel, Field + + class ExampleConfig(BaseModel): + build_dir: Optional[str] = Field(None, alias="buildDir") + + builder_name: str = Field("html", alias="builderName") + + conf_dir: Optional[str] = Field(None, alias="confDir") + +.. code-block:: python + + # After + import attrs + + @attrs.define + class ExampleConfig: + build_dir: Optional[str] = attrs.field(default=None) + + builder_name: str = attrs.field(default="html") + + conf_dir: Optional[str] = attrs.field(default=None) + + +Pygls provides a default `converter`_ that it will use when converting your models to/from JSON, which should be sufficient for most scenarios. + +.. code-block:: pycon + + >>> from pygls.protocol import default_converter + >>> converter = default_converter() + + >>> config = ExampleConfig(builder_name='epub', conf_dir='/path/to/conf') + >>> converter.unstructure(config) + {'builderName': 'epub', 'confDir': '/path/to/conf'} # Note how snake_case is converted to camelCase + + >>> converter.structure({'builderName': 'epub', 'confDir': '/path/to/conf'}, ExampleConfig) + ExampleConfig(build_dir=None, builder_name='epub', conf_dir='/path/to/conf') + +However, depending on the complexity of your type definitions you may find the default converter fail to parse some of your types. + +.. code-block:: pycon + + >>> from typing import Literal, Union + + >>> @attrs.define + ... class ExampleConfig: + ... num_jobs: Union[Literal["auto"], int] = attrs.field(default='auto') + ... + + >>> converter.structure({'numJobs': 'auto'}, ExampleConfig) + + Exception Group Traceback (most recent call last): + | File "<stdin>", line 1, in <module> + | File "/.../python3.10/site-packages/cattrs/converters.py", li + ne 309, in structure + | return self._structure_func.dispatch(cl)(obj, cl) + | File "<cattrs generated structure __main__.ExampleConfig-2>", line 10, in structure_ExampleConfig + | if errors: raise __c_cve('While structuring ' + 'ExampleConfig', errors, __cl) + | cattrs.errors.ClassValidationError: While structuring ExampleConfig (1 sub-exception) + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "<cattrs generated structure __main__.ExampleConfig-2>", line 6, in structure_ExampleConfig + | res['num_jobs'] = __c_structure_num_jobs(o['numJobs'], __c_type_num_jobs) + | File "/.../python3.10/site-packages/cattrs/converters.py", + line 377, in _structure_error + | raise StructureHandlerNotFoundError(msg, type_=cl) + | cattrs.errors.StructureHandlerNotFoundError: Unsupported type: typing.Union[typing.Literal['auto'], int]. + Register a structure hook for it. + | Structuring class ExampleConfig @ attribute num_jobs + +------------------------------------ + +In which case you can extend the converter provided by ``pygls`` with your own `structure hooks`_ + +.. code-block:: python + + from pygls.protocol import default_converter + + def custom_converter(): + converter = default_converter() + converter.register_structure_hook(Union[Literal['auto', int], lambda obj, _: obj) + + return converter + +You can then override the default converter used by ``pygls`` when constructing your language server instance + +.. code-block:: python + + server = LanguageServer( + name="my-language-server", version="v1.0", converter_factory=custom_converter + ) + +See the `hooks.py`_ module in ``lsprotocol`` for some example structure hooks + +Miscellaneous +------------- + +Mandatory ``name`` and ``version`` +"""""""""""""""""""""""""""""""""" + +It is now necessary to provide a name and version when constructing an instance of the ``LanguageServer`` class + +.. code-block:: python + + from pygls.server import LanguageServer + + server = LanguageServer(name="my-language-server", version="v1.0") + + +``ClientCapabilities.get_capability`` is now ``get_capability`` +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +.. code-block:: python + + # Before + from pygls.lsp.types import ClientCapabilities + + client_capabilities = ClientCapabilities() + commit_character_support = client_capabilities.get_capability( + "text_document.completion.completion_item.commit_characters_support", False + ) + +.. code-block:: python + + # After + from lsprotocol.types import ClientCapabilities + from pygls.capabilities import get_capability + + client_capabilities = ClientCapabilities() + commit_character_support = get_capability( + client_capabilities, + "text_document.completion.completion_item.commit_characters_support", + False + ) + +.. _attrs: https://www.attrs.org/en/stable/index.html +.. _cattrs: https://cattrs.readthedocs.io/en/stable/ +.. _converter: https://cattrs.readthedocs.io/en/stable/converters.html +.. _hooks.py: https://github.com/microsoft/lsprotocol/blob/main/lsprotocol/_hooks.py +.. _lsprotocol: https://github.com/microsoft/lsprotocol +.. _pydantic: https://pydantic-docs.helpmanual.io/ +.. _structure hooks: https://cattrs.readthedocs.io/en/stable/structuring.html#registering-custom-structuring-hooks +.. _jedi-language-server: https://github.com/pappasam/jedi-language-server/pull/230 +.. _yara-language-server: https://github.com/avast/yls/pull/34 +.. _vscode-ruff: https://github.com/charliermarsh/vscode-ruff/pull/37 +.. _esbonio: https://github.com/swyddfa/esbonio/pull/484 diff --git a/docs/source/pages/reference.rst b/docs/source/pages/reference.rst new file mode 100644 index 0000000..2b0aa56 --- /dev/null +++ b/docs/source/pages/reference.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. toctree:: + :glob: + :maxdepth: 2 + + reference/* diff --git a/docs/source/pages/reference/clients.rst b/docs/source/pages/reference/clients.rst new file mode 100644 index 0000000..b4c37ed --- /dev/null +++ b/docs/source/pages/reference/clients.rst @@ -0,0 +1,8 @@ +Clients +======= + +.. autoclass:: pygls.lsp.client.BaseLanguageClient + :members: + +.. autoclass:: pygls.client.JsonRPCClient + :members: diff --git a/docs/source/pages/reference/protocol.rst b/docs/source/pages/reference/protocol.rst new file mode 100644 index 0000000..e9bd2f7 --- /dev/null +++ b/docs/source/pages/reference/protocol.rst @@ -0,0 +1,11 @@ +Protocol +======== + + +.. autoclass:: pygls.protocol.LanguageServerProtocol + :members: + +.. autoclass:: pygls.protocol.JsonRPCProtocol + :members: + +.. autofunction:: pygls.protocol.default_converter diff --git a/docs/source/pages/reference/servers.rst b/docs/source/pages/reference/servers.rst new file mode 100644 index 0000000..2a664ed --- /dev/null +++ b/docs/source/pages/reference/servers.rst @@ -0,0 +1,11 @@ +Servers +======= + +.. autoclass:: pygls.server.LanguageServer + :members: + +.. autoclass:: pygls.server.Server + :members: + + + diff --git a/docs/source/pages/reference/types.rst b/docs/source/pages/reference/types.rst new file mode 100644 index 0000000..1cbcb1f --- /dev/null +++ b/docs/source/pages/reference/types.rst @@ -0,0 +1,10 @@ +Types +===== + +LSP type definitions in ``pygls`` are provided by the `lsprotocol <https://github.com/microsoft/lsprotocol>`__ library + +.. automodule:: lsprotocol.types + :members: + :undoc-members: + + diff --git a/docs/source/pages/reference/workspace.rst b/docs/source/pages/reference/workspace.rst new file mode 100644 index 0000000..1e392dc --- /dev/null +++ b/docs/source/pages/reference/workspace.rst @@ -0,0 +1,9 @@ +Workspace +========= + +.. autoclass:: pygls.workspace.TextDocument + :members: + +.. autoclass:: pygls.workspace.Workspace + :members: + diff --git a/docs/source/pages/testing.rst b/docs/source/pages/testing.rst new file mode 100644 index 0000000..9910835 --- /dev/null +++ b/docs/source/pages/testing.rst @@ -0,0 +1,24 @@ +.. _testing: + +Testing +======= + +Unit Tests +---------- + +Writing unit tests for registered features and commands are easy and you don't +have to mock the whole language server. If you skipped the advanced usage page, +take a look at :ref:`passing language server instance <passing-instance>` +section for more details. + +Integration Tests +----------------- + +Integration tests coverage includes the whole workflow, from sending the client +request, to getting the result from the server. Since the *Language Server +Protocol* defines bidirectional communication between the client and the +server, we used *pygls* to simulate the client and send desired requests to the +server. To get a better understanding of how to set it up, take a look at our test +`fixtures`_. + +.. _fixtures: https://github.com/openlawlibrary/pygls/blob/main/tests/conftest.py diff --git a/docs/source/pages/tutorial.rst b/docs/source/pages/tutorial.rst new file mode 100644 index 0000000..6db7f6c --- /dev/null +++ b/docs/source/pages/tutorial.rst @@ -0,0 +1,207 @@ +.. _tutorial: + +Tutorial +======== + +In order to help you with using *pygls* in VSCode, we have created the `vscode-playground`_ extension. + +.. note:: + + This extension is meant to provide an environment in which you can easily experiment with a *pygls* powered language server. + It is not necessary in order to use *pygls* with other text editors. + + If you decide you want to publish your language server on the VSCode marketplace this + `template extension <https://github.com/microsoft/vscode-python-tools-extension-template>`__ + from Microsoft a useful starting point. + +Prerequisites +------------- + +In order to setup and run the example VSCode extension, you need following software +installed: + +* `Visual Studio Code <https://code.visualstudio.com/>`_ editor +* `Python 3.8+ <https://www.python.org/downloads/>`_ +* `vscode-python <https://marketplace.visualstudio.com/items?itemName=ms-python.python>`_ extension +* A clone of the `pygls <https://github.com/openlawlibrary/pygls>`_ repository + +.. note:: + If you have created virtual environment, make sure that you have *pygls* installed + and `selected appropriate python interpreter <https://code.visualstudio.com/docs/python/environments>`_ + for the *pygls* project. + + +Running the Example +------------------- + +For a step-by-step guide on how to setup and run the example follow `README`_. + +Hacking the Extension +--------------------- + +When you have successfully setup and run the extension, open `server.py`_ and +go through the code. + +We have implemented following capabilities: + +- ``textDocument/completion`` feature +- ``countDownBlocking`` command +- ``countDownNonBlocking`` command +- ``textDocument/didChange`` feature +- ``textDocument/didClose`` feature +- ``textDocument/didOpen`` feature +- ``showConfigurationAsync`` command +- ``showConfigurationCallback`` command +- ``showConfigurationThread`` command + +When running the extension in *debug* mode, you can set breakpoints to see +when each of above mentioned actions gets triggered. + +Visual Studio Code supports *Language Server Protocol*, which means, that every +action on the client-side, will result in sending request or notification to +the server via JSON RPC. + +Debug Code Completions +~~~~~~~~~~~~~~~~~~~~~~ + +Set a breakpoint inside ``completion`` function and go back to opened *json* +file in your editor. Now press ``ctrl + space`` (``control + space`` on mac) to +show completion list and you will hit the breakpoint. When you continue +debugging, the completion list pop-up won't show up because it was closing when +the editor lost focus. + +Similarly, you can debug any feature or command. + +Keep the breakpoint and continue to the next section. + +Blocking Command Test +~~~~~~~~~~~~~~~~~~~~~ + +In order to demonstrate you that blocking the language server will reject other +requests, we have registered a custom command which counts down 10 seconds and +sends notification messages to the client. + +1. Press **F1**, find and run ``Count down 10 seconds [Blocking]`` command. +2. Try to show *code completions* while counter is still ticking. + +Language server is **blocked**, because ``time.sleep`` is a +**blocking** operation. This is why you didn't hit the breakpoint this time. + +.. hint:: + + To make this command **non blocking**, add ``@json_server.thread()`` + decorator, like in code below: + + .. code-block:: python + + @json_server.thread() + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING) + def count_down_10_seconds_blocking(ls, *args): + # Omitted + + *pygls* uses a **thread pool** to execute functions that are marked with + a ``thread`` decorator. + + +Non-Blocking Command Test +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Python 3.4 introduced *asyncio* module which allows us to use asynchronous +functions (aka *coroutines*) and do `cooperative multitasking`_. Using the +`await` keyword inside your coroutine will give back control to the +scheduler and won't block the main thread. + +1. Press **F1** and run the ``Count down 10 seconds [Non Blocking]`` command. +2. Try to show *code completions* while counter is still ticking. + +Bingo! We hit the breakpoint! What just happened? + +The language server was **not blocked** because we used ``asyncio.sleep`` this +time. The language server was executing *just* in the *main* thread. + + +Text Document Operations +~~~~~~~~~~~~~~~~~~~~~~~~ + +Opening and closing a JSON file will display appropriate notification message +in the bottom right corner of the window and the file content will be +validated. Validation will be performed on content changes, as well. + +Show Configuration Data +~~~~~~~~~~~~~~~~~~~~~~~ + +There are *three* ways for getting configuration section from the client +settings. + +.. note:: + + *pygls*' built-in coroutines are suffixed with *async* word, which means that + you have to use the *await* keyword in order to get the result (instead of + *asyncio.Future* object). + +- **Get the configuration inside a coroutine** + +.. code-block:: python + + config = await ls.get_configuration_async(ConfigurationParams([ + ConfigurationItem('', JsonLanguageServer.CONFIGURATION_SECTION) + ])) + +- **Get the configuration inside a normal function** + +We already saw that we *don't* want to block the main thread. Sending the +configuration request to the client will result with the response from it, but +we don't know when. You have to pass *callback* function which will be +triggered once response from the client is received. + +.. code-block:: python + + def _config_callback(config): + try: + example_config = config[0].exampleConfiguration + + ls.show_message( + f'jsonServer.exampleConfiguration value: {example_config}' + ) + + except Exception as e: + ls.show_message_log(f'Error ocurred: {e}') + + ls.get_configuration(ConfigurationParams([ + ConfigurationItem('', JsonLanguageServer.CONFIGURATION_SECTION) + ]), _config_callback) + +As you can see, the above code is hard to read. + +- **Get the configuration inside a threaded function** + +Blocking operations such as ``future.result(1)`` should not be used inside +normal functions, but to increase the code readability, you can add the +*thread* decorator to your function to use *pygls*' *thread pool*. + +.. code-block:: python + + @json_server.thread() + @json_server.command(JsonLanguageServer.CMD_SHOW_CONFIGURATION_THREAD) + def show_configuration_thread(ls: JsonLanguageServer, *args): + """Gets exampleConfiguration from the client settings using a thread pool.""" + try: + config = ls.get_configuration(ConfigurationParams([ + ConfigurationItem('', JsonLanguageServer.CONFIGURATION_SECTION) + ])).result(2) + + # ... + +This way you won't block the main thread. *pygls* will start a new thread when +executing the function. + +Modify the Example +~~~~~~~~~~~~~~~~~~ + +We encourage you to continue to :ref:`user guide <user-guide>` and +modify this example. + +.. _vscode-playground: https://github.com/openlawlibrary/pygls/blob/main/examples/vscode-playground +.. _README: https://github.com/openlawlibrary/pygls/blob/main/examples/vscode-playground/README.md +.. _server.py: https://github.com/openlawlibrary/pygls/blob/main/examples/servers/json_server.py +.. _cooperative multitasking: https://en.wikipedia.org/wiki/Cooperative_multitasking diff --git a/docs/source/pages/user-guide.rst b/docs/source/pages/user-guide.rst new file mode 100644 index 0000000..c0ce43e --- /dev/null +++ b/docs/source/pages/user-guide.rst @@ -0,0 +1,536 @@ +.. _user-guide: + +User Guide +========== + +Language Server +--------------- + +The language server is responsible for managing the connection with the client as well as sending and receiving messages over +the `Language Server Protocol <https://microsoft.github.io/language-server-protocol/>`__ +which is based on the `Json RPC protocol <https://www.jsonrpc.org/specification>`__. + +Connections +~~~~~~~~~~~ + +*pygls* supports :ref:`ls-tcp`, :ref:`ls-stdio` and :ref:`ls-websocket` connections. + +.. _ls-tcp: + +TCP +^^^ + +TCP connections are usually used while developing the language server. +This way the server can be started in *debug* mode separately and wait +for the client connection. + +.. note:: Server should be started **before** the client. + +The code snippet below shows how to start the server in *TCP* mode. + +.. code:: python + + from pygls.server import LanguageServer + + server = LanguageServer('example-server', 'v0.1') + + server.start_tcp('127.0.0.1', 8080) + +.. _ls-stdio: + +STDIO +^^^^^ + +STDIO connections are useful when client is starting the server as a child +process. This is the way to go in production. + +The code snippet below shows how to start the server in *STDIO* mode. + +.. code:: python + + from pygls.server import LanguageServer + + server = LanguageServer('example-server', 'v0.1') + + server.start_io() + +.. _ls-websocket: + +WEBSOCKET +^^^^^^^^^ + +WEBSOCKET connections are used when you want to expose language server to +browser based editors. + +The code snippet below shows how to start the server in *WEBSOCKET* mode. + +.. code:: python + + from pygls.server import LanguageServer + + server = LanguageServer('example-server', 'v0.1') + + server.start_ws('0.0.0.0', 1234) + +Logging +~~~~~~~ + +Logs are useful for tracing client requests, finding out errors and +measuring time needed to return results to the client. + +*pygls* uses built-in python *logging* module which has to be configured +before server is started. + +Official documentation about logging in python can be found +`here <https://docs.python.org/3/howto/logging-cookbook.html>`__. Below +is the minimal setup to setup logging in *pygls*: + +.. code:: python + + import logging + + from pygls.server import LanguageServer + + logging.basicConfig(filename='pygls.log', filemode='w', level=logging.DEBUG) + + server = LanguageServer('example-server', 'v0.1') + + server.start_io() + +Overriding ``LanguageServerProtocol`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a reason to override the existing ``LanguageServerProtocol`` class, +you can do that by inheriting the class and passing it to the ``LanguageServer`` +constructor. + +Custom Error Reporting +~~~~~~~~~~~~~~~~~~~~~~ + +The default :class:`~pygls.server.LanguageServer` will send a :lsp:`window/showMessage` notification to the client to display any uncaught exceptions in the server. +To override this behaviour define your own :meth:`~pygls.server.LanguageServer.report_server_error` method like so: + +.. code:: python + + class CustomLanguageServer(LanguageServer): + def report_server_error(self, error: Exception, source: Union[PyglsError, JsonRpcException]): + pass + +Handling Client Messages +------------------------ + +.. admonition:: Requests vs Notifications + + Unlike a *request*, a *notification* message has no ``id`` field and the server *must not* reply to it. + This means that, even if you return a result inside a handler function for a notification, the result won't be passed to the client. + + The ``Language Server Protocol``, unlike ``Json RPC``, allows bidirectional communication between the server and the client. + +For the majority of the time, a language server will be responding to requests and notifications sent from the client. +*pygls* refers to the handlers for all of these messages as *features* with one exception. + +The Language Server protocol allows a server to define named methods that a client can invoke by sending a :lsp:`workspace/executeCommand` request. +Unsurprisingly, *pygls* refers to these named methods a *commands*. + +*Built-In* Features +~~~~~~~~~~~~~~~~~~~ + +*pygls* comes with following predefined set of handlers for the following +`Language Server Protocol <https://microsoft.github.io/language-server-protocol/>`__ +(LSP) features: + +.. note:: + + *Built-in* features in most cases should *not* be overridden. + + If you need to do some additional processing of one of the messages listed below, register a feature with the same name and your handler will be called immediately after the corresponding built-in feature. + +**Lifecycle Messages** + +- The :lsp:`initialize` request is sent as a first request from client to the server to setup their communication. + *pygls* automatically computes registered LSP capabilities and sends them as part of the :class:`~lsprotocol.types.InitializeResult` response. + +- The :lsp:`shutdown` request is sent from the client to the server to ask the server to shutdown. + +- The :lsp:`exit` notification is sent from client to the server to ask the server to exit the process. + *pygls* automatically releases all resources and stops the process. + +**Text Document Synchronization** + +- The :lsp:`textDocument/didOpen` notification will tell *pygls* to create a document in the in-memory workspace which will exist as long as the document is opened in editor. + +- The :lsp:`textDocument/didChange` notification will tell *pygls* to update the document text. + *pygls* supports *full* and *incremental* document changes. + +- The :lsp:`textDocument/didClose` notification will tell *pygls* to remove a document from the in-memory workspace. + +**Notebook Document Synchronization** + +- The :lsp:`notebookDocument/didOpen` notification will tell *pygls* to create a notebook document in the in-memory workspace which will exist as long as the document is opened in editor. + +- The :lsp:`notebookDocument/didChange` notification will tell *pygls* to update the notebook document include its content, metadata, execution results and cell structure. + +- The :lsp:`notebookDocument/didClose` notification will tell *pygls* to remove the notebook from the in-memory workspace. + +**Miscellanous** + +- The :lsp:`workspace/didChangeWorkspaceFolders` notification will tell *pygls* to update in-memory workspace folders. + +- The :lsp:`workspace/executeCommand` request will tell *pygls* to execute a custom command. + +- The :lsp:`$/setTrace` notification tells *pygls* to update the server's :class:`TraceValue <lsprotocol.types.TraceValues>`. + +.. _ls-handlers: + +Registering Handlers +~~~~~~~~~~~~~~~~~~~~ + +.. seealso:: + + It's recommeded that you follow the :ref:`tutorial <tutorial>` before reading this section. + +- The :func:`~pygls.server.LanguageServer.feature` decorator is used to register a handler for a given LSP message. +- The :func:`~pygls.server.LanguageServer.command` decorator is used to register a named command. + +The following applies to both feature and command handlers. + +Language servers using *pygls* run in an *asyncio event loop*. +They *asynchronously* listen for incoming messages and, depending on the way handler is registered, apply different execution strategies to process the message. + +Depending on the use case, handlers can be registered in three different ways: + +- as an :ref:`async <ls-handler-async>` function +- as a :ref:`synchronous <ls-handler-sync>` function +- as a :ref:`threaded <ls-handler-thread>` function + +.. _ls-handler-async: + +*Asynchronous* Functions (*Coroutines*) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*pygls* supports ``python 3.8+`` which has a keyword ``async`` to +specify coroutines. + +The code snippet below shows how to register a command as a coroutine: + +.. code:: python + + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_NON_BLOCKING) + async def count_down_10_seconds_non_blocking(ls, *args): + # Omitted + +Registering a *feature* as a coroutine is exactly the same. + +Coroutines are functions that are executed as tasks in *pygls*'s *event +loop*. They should contain at least one *await* expression (see +`awaitables <https://docs.python.org/3.5/glossary.html#term-awaitable>`__ +for details) which tells event loop to switch to another task while +waiting. This allows *pygls* to listen for client requests in a +*non blocking* way, while still only running in the *main* thread. + +Tasks can be canceled by the client if they didn't start executing (see +`Cancellation +Support <https://microsoft.github.io/language-server-protocol/specification#cancelRequest>`__). + +.. warning:: + + Using computation intensive operations will *block* the main thread and + should be *avoided* inside coroutines. Take a look at + `threaded functions <#threaded-functions>`__ for more details. + +.. _ls-handler-sync: + +*Synchronous* Functions +^^^^^^^^^^^^^^^^^^^^^^^ + +Synchronous functions are regular functions which *blocks* the *main* +thread until they are executed. + +`Built-in features <#built-in-features>`__ are registered as regular +functions to ensure correct state of language server initialization and +workspace. + +The code snippet below shows how to register a command as a regular +function: + +.. code:: python + + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING) + def count_down_10_seconds_blocking(ls, *args): + # Omitted + +Registering *feature* as a regular function is exactly the same. + +.. warning:: + + Using computation intensive operations will *block* the main thread and + should be *avoided* inside regular functions. Take a look at + `threaded functions <#threaded-functions>`__ for more details. + +.. _ls-handler-thread: + +*Threaded* Functions +^^^^^^^^^^^^^^^^^^^^ + +*Threaded* functions are just regular functions, but marked with +*pygls*'s ``thread`` decorator: + +.. code:: python + + # Decorator order is not important in this case + @json_server.thread() + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING) + def count_down_10_seconds_blocking(ls, *args): + # Omitted + +*pygls* uses its own *thread pool* to execute above function in *daemon* +thread and it is *lazy* initialized first time when function marked with +``thread`` decorator is fired. + +*Threaded* functions can be used to run blocking operations. If it has been a +while or you are new to threading in Python, check out Python's +``multithreading`` and `GIL <https://en.wikipedia.org/wiki/Global_interpreter_lock>`__ +before messing with threads. + +.. _passing-instance: + +Passing Language Server Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using language server methods inside registered features and commands are quite +common. We recommend adding language server as a **first parameter** of a +registered function. + +There are two ways of doing this: + +- **ls** (**l**\anguage **s**\erver) naming convention + +Add **ls** as first parameter of a function and *pygls* will automatically pass +the language server instance. + +.. code-block:: python + + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING) + def count_down_10_seconds_blocking(ls, *args): + # Omitted + + +- add **type** to first parameter + +Add the **LanguageServer** class or any class derived from it as a type to +first parameter of a function + +.. code-block:: python + + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING) + def count_down_10_seconds_blocking(ser: JsonLanguageServer, *args): + # Omitted + + +Using outer ``json_server`` instance inside registered function will make +writing unit :ref:`tests <testing>` more difficult. + +Communicating with the Client +----------------------------- + +.. important:: + + Most of the messages listed here cannot be sent until the LSP session has been initialized. + See the section on the :lsp:`initiaiize` request in the specification for more details. + +In addition to responding to requests, there are a number of additional messages a server can send to the client. + +Configuration +~~~~~~~~~~~~~ + +The :lsp:`workspace/configuration` request is sent from the server to the client in order to fetch configuration settings from the client. +Depending on how the handler is registered (see :ref:`here <ls-handlers>`) you can use the :meth:`~pygls.server.LanguageServer.get_configuration` or :meth:`~pygls.server.LanguageServer.get_configuration_async` methods to request configuration from the client: + +- *asynchronous* functions (*coroutines*) + + .. code:: python + + # await keyword tells event loop to switch to another task until notification is received + config = await ls.get_configuration( + WorkspaceConfigurationParams( + items=[ + ConfigurationItem(scope_uri='doc_uri_here', section='section') + ] + ) + ) + +- *synchronous* functions + + .. code:: python + + # callback is called when notification is received + def callback(config): + # Omitted + + params = WorkspaceConfigurationParams( + items=[ + ConfigurationItem(scope_uri='doc_uri_here', section='section') + ] + ) + config = ls.get_configuration(params, callback) + +- *threaded* functions + + .. code:: python + + # .result() will block the thread + config = ls.get_configuration( + WorkspaceConfigurationParams( + items=[ + ConfigurationItem(scope_uri='doc_uri_here', section='section') + ] + ) + ).result() + +Publish Diagnostics +~~~~~~~~~~~~~~~~~~~ + +:lsp:`textDocument/publishDiagnostics` notifications are sent from the server to the client to highlight errors or potential issues. e.g. syntax errors or unused variables. + +Usually this notification is sent after document is opened, or on document content change: + +.. code:: python + + @json_server.feature(TEXT_DOCUMENT_DID_OPEN) + async def did_open(ls, params: DidOpenTextDocumentParams): + """Text document did open notification.""" + ls.show_message("Text Document Did Open") + ls.show_message_log("Validating json...") + + # Get document from workspace + text_doc = ls.workspace.get_text_document(params.text_document.uri) + + diagnostic = Diagnostic( + range=Range( + start=Position(line-1, col-1), + end=Position(line-1, col) + ), + message="Custom validation message", + source="Json Server" + ) + + # Send diagnostics + ls.publish_diagnostics(text_doc.uri, [diagnostic]) + +Show Message +~~~~~~~~~~~~ + +:lsp:`window/showMessage` is a notification that is sent from the server to the client to display a prominant text message. e.g. VSCode will render this as a notification popup + +.. code:: python + + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_NON_BLOCKING) + async def count_down_10_seconds_non_blocking(ls, *args): + for i in range(10): + # Sends message notification to the client + ls.show_message(f"Counting down... {10 - i}") + await asyncio.sleep(1) + +Show Message Log +~~~~~~~~~~~~~~~~ + +:lsp:`window/logMessage` is a notification that is sent from the server to the client to display a discrete text message. e.g. VSCode will display the message in an :guilabel:`Output` channel. + +.. code:: python + + @json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_NON_BLOCKING) + async def count_down_10_seconds_non_blocking(ls, *args): + for i in range(10): + # Sends message log notification to the client + ls.show_message_log(f"Counting down... {10 - i}") + await asyncio.sleep(1) + +Workspace Edits +~~~~~~~~~~~~~~~ + +The :lsp:`workspace/applyEdit` request allows your language server to ask the client to modify particular documents in the client's workspace. + +.. code:: python + + def apply_edit(self, edit: WorkspaceEdit, label: str = None) -> ApplyWorkspaceEditResponse: + # Omitted + + def apply_edit_async(self, edit: WorkspaceEdit, label: str = None) -> ApplyWorkspaceEditResponse: + # Omitted + +Custom Notifications +~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + + Custom notifications are not part of the LSP specification and dedicated support for your custom notification(s) will have to be added to each language client you intend to support. + +A custom notification can be sent to the client using the :meth:`~pygls.server.LanguageServer.send_notification` method + +.. code:: python + + server.send_notification('myCustomNotification', 'test data') + + +The Workspace +------------- + +The :class:`~pygls.workspace.Workspace` is a python object that holds information about workspace folders, opened documents and is responsible for synchronising server side document state with that of the client. + +**Text Documents** + +The :class:`~pygls.workspace.TextDocument` class is how *pygls* represents a text document. +Given a text document's uri the :meth:`~pygls.workspace.Workspace.get_text_document` method can be used to access the document itself: + +.. code:: python + + @json_server.feature(TEXT_DOCUMENT_DID_OPEN) + async def did_open(ls, params: DidOpenTextDocumentParams): + + # Get document from workspace + text_doc = ls.workspace.get_text_document(params.text_document.uri) + +**Notebook Documents** + +.. seealso:: + + See the section on :lsp:`notebookDocument/synchronization` in the specification for full details on how notebook documents are handled + +- A notebook's structure, metadata etc. is represented using the :class:`~lsprotocol.types.NotebookDocument` class from ``lsprotocol``. +- The contents of a single notebook cell is represented using a standard :class:`~pygls.workspace.TextDocument` + +In order to receive notebook documents from the client, your language server must provide an instance of :class:`~lsprotocol.types.NotebookDocumentSyncOptions` which declares the kind of notebooks it is interested in + +.. code-block:: python + + server = LanguageServer( + name="example-server", + version="v0.1", + notebook_document_sync=types.NotebookDocumentSyncOptions( + notebook_selector=[ + types.NotebookDocumentSyncOptionsNotebookSelectorType2( + cells=[ + types.NotebookDocumentSyncOptionsNotebookSelectorType2CellsType( + language="python" + ) + ] + ) + ] + ), + ) + +To access the contents of a notebook cell you would call the workspace's :meth:`~pygls.workspace.Workspace.get_text_document` method as normal. + +.. code-block:: python + + cell_doc = ls.workspace.get_text_document(cell_uri) + +To access the notebook itself call the workspace's :meth:`~pygls.workspace.Workspace.get_notebook_document` method with either the uri of the notebook *or* the uri of any of its cells. + +.. code-block:: python + + notebook_doc = ls.workspace.get_notebook_document(notebook_uri=notebook_uri) + + # -- OR -- + + notebook_doc = ls.workspace.get_notebook_document(cell_uri=cell_uri) |