diff --git a/docs/source/conf.py b/docs/source/conf.py
index 1b4dc01c..b3b3bfc9 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -184,7 +184,7 @@
def lsp_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
"""Link to sections within the lsp specification."""
- anchor = text.replace("/", "_")
+ anchor = text.replace("$/", "").replace("/", "_")
ref = f"https://microsoft.github.io/language-server-protocol/specification.html#{anchor}"
node = nodes.reference(rawtext, text, refuri=ref, **options)
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8705175d..f9099019 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -40,7 +40,7 @@ User Guide
pages/getting_started
pages/tutorial
- pages/advanced_usage
+ pages/user-guide
pages/testing
pages/migrating-to-v1
pages/reference
diff --git a/docs/source/pages/advanced_usage.rst b/docs/source/pages/advanced_usage.rst
deleted file mode 100644
index cbedfd3f..00000000
--- a/docs/source/pages/advanced_usage.rst
+++ /dev/null
@@ -1,495 +0,0 @@
-.. _advanced-usage:
-
-Advanced Usage
-==============
-
-Language Server
----------------
-
-The language server is responsible for receiving and sending messages over
-the `Language Server Protocol `__
-which is based on the `Json RPC protocol `__.
-
-Connections
-~~~~~~~~~~~
-
-*pygls* supports *TCP* and socket *STDIO* connections.
-
-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)
-
-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()
-
-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_websocket('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 `__. 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.
-
-Features
---------
-
-What is a feature in *pygls*? In terms of language servers and the
-`Language Server Protocol `__,
-a feature is one of the predefined methods from
-LSP `specification `__,
-such as: *code completion*, *formatting*, *code lens*, etc. See the `lsprotocol
-`_ module
-for the complete and canonical list of available features.
-
-*Built-In* Features
-~~~~~~~~~~~~~~~~~~~
-
-*pygls* comes with following predefined set of
-`Language Server Protocol `__
-(LSP) features:
-
-- The `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 ``InitializeResult`` response.
-
-- The `shutdown `__
- request is sent from the client to the server to ask the server to
- shutdown.
-
-- The `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.
-
-- The `textDocument/didOpen `__
- notification will tell *pygls* to create a document in the in-memory
- workspace which will exist as long as document is opened in editor.
-
-- The `textDocument/didChange `__
- notification will tell *pygls* to update the document text.
- *pygls* supports _full_ and _incremental_ document changes.
-
-- The `textDocument/didClose `__
- notification will tell *pygls* to remove a document from the
- in-memory workspace.
-
-- The `workspace/didChangeWorkspaceFolders `__
- notification will tell *pygls* to update in-memory workspace folders.
-
-Commands
---------
-
-Commands can be treated as a *custom features*, i.e. everything that is
-not covered by LSP specification, but needs to be implemented.
-
-API
----
-
-*Feature* and *Command* Advanced Registration
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-*pygls* is a language server which relies on *asyncio event loop*. It is
-*asynchronously* listening for incoming messages and, depending on the
-way method is registered, applying different execution strategies to
-respond to the client.
-
-Depending on the use case, *features* and *commands* can be registered
-in three different ways.
-
-To make sure that you fully understand what is happening under the hood,
-please take a look at the :ref:`tutorial `.
-
-.. note::
-
- *Built-in* features in most cases should *not* be overridden.
- Instead, register the feature with the same name and it will be
- called immediately after the corresponding built-in feature.
-
-*Asynchronous* Functions (*Coroutines*)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-*pygls* supports ``python 3.7+`` 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 `__
-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 `__).
-
-.. 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.
-
-*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.
-
-*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 `__
-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 ` more difficult.
-
-
-Notifications
-~~~~~~~~~~~~~
-
-A *notification* is a request message without the ``id`` field and server
-*must not* reply to it. This means that, if your language server received the
-notification, even if you return the result inside your handler function,
-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.
-
-Configuration
-^^^^^^^^^^^^^
-
-The `configuration `__
-request is sent from the server to the client in order to fetch
-configuration settings from the client. When the requested configuration
-is collected, the client sends data as a notification to the server.
-
-.. note::
-
- Although ``configuration`` is a ``request``, it is explained in this
- section because the client sends back the ``notification`` object.
-
-The code snippet below shows how to send configuration to the client:
-
-.. code:: python
-
- def get_configuration(self,
- params: WorkspaceConfigurationParams,
- callback: Optional[Callable[[List[Any]], None]] = None
- ) -> asyncio.Future:
- # Omitted
-
-*pygls* has three ways for handling configuration notification from the
-client, depending on way how the function is registered (described
-`here <#feature-and-command-advanced-registration>`__):
-
-- *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
-
- config = ls.get_configuration(WorkspaceConfigurationParams(items=[ConfigurationItem(scope_uri='doc_uri_here', section='section')]), 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()
-
-Show Message
-^^^^^^^^^^^^
-
-`Show
-message `__
-is notification that is sent from the server to the client to display
-text message.
-
-The code snippet below shows how to send show message notification:
-
-.. 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
-^^^^^^^^^^^^^^^^
-
-`Show message
-log `__
-is notification that is sent from the server to the client to display
-text message in the output channel.
-
-The code snippet below shows how to send show message log notification:
-
-.. 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's output channel
- ls.show_message_log(f"Counting down... {10 - i}")
- await asyncio.sleep(1)
-
-Publish Diagnostics
-^^^^^^^^^^^^^^^^^^^
-
-`Publish
-diagnostics `__
-notifications are sent from the server to the client to signal results
-of validation runs.
-
-Usually this notification is sent after document is opened, or on
-document content change, e.g.:
-
-.. 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_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])
-
-
-Custom Notifications
-^^^^^^^^^^^^^^^^^^^^
-
-*pygls* supports sending custom notifications to the client and below
-is method declaration for this functionality:
-
-.. code:: python
-
- def send_notification(self, method: str, params: object = None) -> None:
- # Omitted
-
-And method invocation example:
-
-.. code:: python
-
- server.send_notification('myCustomNotification', 'test data')
-
-Custom Error Reporting
-^^^^^^^^^^^^^^^^^^^^^^
-
-By default Pygls notifies the client to display any occurences of uncaught exceptions in the
-server. To override this behaviour define your own `report_server_error()` method like so:
-
-.. code:: python
-
- Class CustomLanguageServer(LanguageServer):
- def report_server_error(self, error: Exception, source: Union[PyglsError, JsonRpcException]):
- pass
-
-
-Workspace
-~~~~~~~~~
-
-`Workspace `__
-is a python object that holds information about workspace folders, opened
-documents and has the logic for updating document content.
-
-*pygls* automatically take care about mentioned features of the
-workspace.
-
-Workspace methods that can be used for user defined features are:
-
-- Get document from the workspace
-
-.. code:: python
-
- def get_document(self, doc_uri: str) -> Document:
- # Omitted
-
-- `Apply
- edit `__
- request
-
-.. 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
-
-.. _pygls.lsp.methods: https://github.com/openlawlibrary/pygls/blob/master/pygls/lsp/methods.py
-.. _pygls.lsp.types: https://github.com/openlawlibrary/pygls/tree/master/pygls/lsp/types
diff --git a/docs/source/pages/getting_started.rst b/docs/source/pages/getting_started.rst
index d8286604..e73d20fa 100644
--- a/docs/source/pages/getting_started.rst
+++ b/docs/source/pages/getting_started.rst
@@ -6,12 +6,12 @@ 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:
+ 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 `_
- - `Language Server Protocol Specification `_
- - `Language Server Protocol SDKs `_
+ - `Language Server Protocol Overview `_
+ - `Language Server Protocol Specification `_
+ - `Language Server Protocol SDKs `_
Installation
@@ -23,12 +23,11 @@ To get the latest release from *PyPI*, simply run:
pip install pygls
-Alternatively, *pygls* source code can be downloaded from our `GitHub`_
-page and installed with following command:
+Alternatively, *pygls* source code can be downloaded from our `GitHub`_ page and installed with following command:
.. code:: console
- python setup.py install
+ pip install git+https://github.com/openlawlibrary/pygls
Quick Start
-----------
@@ -78,13 +77,7 @@ Register Features and Commands
def cmd_return_hello_world(ls, *args):
return 'Hello World!'
-See the `lsprotocol`_ module for the complete and canonical list of available features.
-
-Advanced usage
---------------
-
-To reveal the full potential of *pygls* (``thread management``, ``coroutines``,
-``multi-root workspace``, ``TCP/STDIO communication``, etc.) keep reading.
+See the :mod:`lsprotocol.types` module for the complete and canonical list of available features.
Tutorial
--------
@@ -92,6 +85,10 @@ Tutorial
We recommend completing the :ref:`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
-.. _lsprotocol: https://github.com/microsoft/lsprotocol/blob/main/packages/python/lsprotocol/types.py
diff --git a/docs/source/pages/reference.rst b/docs/source/pages/reference.rst
index 3e9b4217..2b0aa56b 100644
--- a/docs/source/pages/reference.rst
+++ b/docs/source/pages/reference.rst
@@ -1,5 +1,5 @@
-Reference
-=========
+API Reference
+=============
.. toctree::
:glob:
diff --git a/docs/source/pages/reference/protocol.rst b/docs/source/pages/reference/protocol.rst
new file mode 100644
index 00000000..e9bd2f77
--- /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 00000000..2a664ed0
--- /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
index 1f251bc5..1cbcb1fe 100644
--- a/docs/source/pages/reference/types.rst
+++ b/docs/source/pages/reference/types.rst
@@ -5,3 +5,6 @@ LSP type definitions in ``pygls`` are provided by the `lsprotocol `
section for more details.
-Json Extension example's `unit tests`_ might be helpful, too.
-
Integration Tests
-----------------
@@ -23,6 +21,4 @@ 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`_.
-
-.. _unit tests: https://github.com/openlawlibrary/pygls/blob/master/examples/json-vscode-extension/server/tests/unit
-.. _fixtures: https://github.com/openlawlibrary/pygls/blob/master/tests/conftest.py#L29
+.. _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
index 44c68847..452cef18 100644
--- a/docs/source/pages/tutorial.rst
+++ b/docs/source/pages/tutorial.rst
@@ -3,10 +3,16 @@
Tutorial
========
-In order to help you with using *pygls* in VSCode, we have created a simple `json-extension`_ example.
+In order to help you with using *pygls* in VSCode, we have created the `vscode-playground`_ extension.
.. note::
- You do not need this extension when using *pygls* with other text editors.
+
+ 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 `__
+ from Microsoft a useful starting point.
Prerequisites
-------------
@@ -82,18 +88,19 @@ 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
+ 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
+ @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.
+ *pygls* uses a **thread pool** to execute functions that are marked with
+ a ``thread`` decorator.
Non-Blocking Command Test
@@ -191,10 +198,10 @@ executing the function.
Modify the Example
~~~~~~~~~~~~~~~~~~
-We encourage you to continue to :ref:`advanced section ` and
+We encourage you to continue to :ref:`user guide ` and
modify this example.
-.. _json-extension: https://github.com/openlawlibrary/pygls/blob/master/examples/json-vscode-extension
-.. _README: https://github.com/openlawlibrary/pygls/blob/master/examples/json-vscode-extension/README.md
-.. _server.py: https://github.com/openlawlibrary/pygls/blob/master/examples/json-vscode-extension/server/server.py
+.. _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 00000000..efe26cab
--- /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 `__
+which is based on the `Json RPC protocol `__.
+
+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 `__. 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 `__
+(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 `.
+
+.. _ls-handlers:
+
+Registering Handlers
+~~~~~~~~~~~~~~~~~~~~
+
+.. seealso::
+
+ It's recommeded that you follow the :ref:`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 ` function
+- as a :ref:`synchronous ` function
+- as a :ref:`threaded ` function
+
+.. _ls-handler-async:
+
+*Asynchronous* Functions (*Coroutines*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+*pygls* supports ``python 3.7+`` 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 `__
+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 `__).
+
+.. 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 `__
+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 ` 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 `) 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)
diff --git a/examples/servers/inlay_hints.py b/examples/servers/inlay_hints.py
index a5e3a6c2..e9924dd7 100644
--- a/examples/servers/inlay_hints.py
+++ b/examples/servers/inlay_hints.py
@@ -17,18 +17,29 @@
import re
from typing import Optional
+from lsprotocol import types
+
from pygls.server import LanguageServer
-from lsprotocol.types import (
- INLAY_HINT_RESOLVE,
- TEXT_DOCUMENT_INLAY_HINT,
- InlayHint,
- InlayHintKind,
- InlayHintParams,
- Position,
-)
NUMBER = re.compile(r"\d+")
-server = LanguageServer("inlay-hint-server", "v0.1")
+COMMENT = re.compile(r"^#$")
+
+
+server = LanguageServer(
+ name="inlay-hint-server",
+ version="v0.1",
+ notebook_document_sync=types.NotebookDocumentSyncOptions(
+ notebook_selector=[
+ types.NotebookDocumentSyncOptionsNotebookSelectorType2(
+ cells=[
+ types.NotebookDocumentSyncOptionsNotebookSelectorType2CellsType(
+ language="python"
+ )
+ ]
+ )
+ ]
+ ),
+)
def parse_int(chars: str) -> Optional[int]:
@@ -38,17 +49,36 @@ def parse_int(chars: str) -> Optional[int]:
return None
-@server.feature(TEXT_DOCUMENT_INLAY_HINT)
-def inlay_hints(params: InlayHintParams):
+@server.feature(types.TEXT_DOCUMENT_INLAY_HINT)
+def inlay_hints(params: types.InlayHintParams):
items = []
document_uri = params.text_document.uri
- document = server.workspace.get_document(document_uri)
+ document = server.workspace.get_text_document(document_uri)
start_line = params.range.start.line
end_line = params.range.end.line
lines = document.lines[start_line : end_line + 1]
for lineno, line in enumerate(lines):
+ match = COMMENT.match(line)
+ if match is not None:
+ nb = server.workspace.get_notebook_document(cell_uri=document_uri)
+ if nb is not None:
+ idx = 0
+ for idx, cell in enumerate(nb.cells):
+ if cell.document == document_uri:
+ break
+
+ items.append(
+ types.InlayHint(
+ label=f"notebook: {nb.uri}, cell {idx+1}",
+ kind=types.InlayHintKind.Type,
+ padding_left=False,
+ padding_right=True,
+ position=types.Position(line=lineno, character=match.end()),
+ )
+ )
+
for match in NUMBER.finditer(line):
if not match:
continue
@@ -59,22 +89,26 @@ def inlay_hints(params: InlayHintParams):
binary_num = bin(number).split("b")[1]
items.append(
- InlayHint(
+ types.InlayHint(
label=f":{binary_num}",
- kind=InlayHintKind.Type,
+ kind=types.InlayHintKind.Type,
padding_left=False,
padding_right=True,
- position=Position(line=lineno, character=match.end()),
+ position=types.Position(line=lineno, character=match.end()),
)
)
return items
-@server.feature(INLAY_HINT_RESOLVE)
-def inlay_hint_resolve(hint: InlayHint):
- n = int(hint.label[1:], 2)
- hint.tooltip = f"Binary representation of the number: {n}"
+@server.feature(types.INLAY_HINT_RESOLVE)
+def inlay_hint_resolve(hint: types.InlayHint):
+ try:
+ n = int(hint.label[1:], 2)
+ hint.tooltip = f"Binary representation of the number: {n}"
+ except Exception:
+ pass
+
return hint
diff --git a/examples/servers/json_server.py b/examples/servers/json_server.py
index 93a171f4..d14ec699 100644
--- a/examples/servers/json_server.py
+++ b/examples/servers/json_server.py
@@ -110,7 +110,7 @@ def workspace_diagnostic(
params: lsp.WorkspaceDiagnosticParams,
) -> lsp.WorkspaceDiagnosticReport:
"""Returns diagnostic report."""
- first = list(json_server.workspace._docs.keys())[0]
+ first = list(json_server.workspace.text_documents.keys())[0]
document = json_server.workspace.get_document(first)
return lsp.WorkspaceDiagnosticReport(
items=[
diff --git a/examples/workspace/Untitled-1.ipynb b/examples/workspace/Untitled-1.ipynb
new file mode 100644
index 00000000..d45e746f
--- /dev/null
+++ b/examples/workspace/Untitled-1.ipynb
@@ -0,0 +1,44 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "12\n",
+ "#"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "mykey": 3
+ },
+ "outputs": [],
+ "source": [
+ "#"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.11.4"
+ },
+ "orig_nbformat": 4
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/pygls/capabilities.py b/pygls/capabilities.py
index 8adcb575..c36832b0 100644
--- a/pygls/capabilities.py
+++ b/pygls/capabilities.py
@@ -15,7 +15,7 @@
# limitations under the License. #
############################################################################
from functools import reduce
-from typing import Any
+from typing import Any, Dict, List, Set, Union
from lsprotocol.types import (
INLAY_HINT_RESOLVE,
@@ -72,12 +72,14 @@
DocumentLinkOptions,
ExecuteCommandOptions,
ImplementationOptions,
+ NotebookDocumentSyncOptions,
SemanticTokensOptions,
SemanticTokensRegistrationOptions,
SemanticTokensOptionsFullType1,
ServerCapabilities,
ServerCapabilitiesWorkspaceType,
SignatureHelpOptions,
+ TextDocumentSyncKind,
TextDocumentSyncOptions,
TypeDefinitionOptions,
FileOperationOptions,
@@ -107,13 +109,20 @@ class ServerCapabilitiesBuilder:
"""
def __init__(
- self, client_capabilities, features, feature_options, commands, sync_kind
+ self,
+ client_capabilities: ClientCapabilities,
+ features: Set[str],
+ feature_options: Dict[str, Any],
+ commands: List[str],
+ text_document_sync_kind: TextDocumentSyncKind,
+ notebook_document_sync: NotebookDocumentSyncOptions,
):
self.client_capabilities = client_capabilities
self.features = features
self.feature_options = feature_options
self.commands = commands
- self.sync_kind = sync_kind
+ self.text_document_sync_kind = text_document_sync_kind
+ self.notebook_document_sync = notebook_document_sync
self.server_cap = ServerCapabilities()
@@ -122,7 +131,7 @@ def _provider_options(self, feature, default=True):
return self.feature_options.get(feature, default)
return None
- def _with_text_doc_sync(self):
+ def _with_text_document_sync(self):
open_close = (
TEXT_DOCUMENT_DID_OPEN in self.features
or TEXT_DOCUMENT_DID_CLOSE in self.features
@@ -147,7 +156,7 @@ def _with_text_doc_sync(self):
self.server_cap.text_document_sync = TextDocumentSyncOptions(
open_close=open_close,
- change=self.sync_kind,
+ change=self.text_document_sync_kind,
will_save=will_save,
will_save_wait_until=will_save_wait_until,
save=save,
@@ -155,6 +164,13 @@ def _with_text_doc_sync(self):
return self
+ def _with_notebook_document_sync(self):
+ if self.client_capabilities.notebook_document is None:
+ return self
+
+ self.server_cap.notebook_document_sync = self.notebook_document_sync
+ return self
+
def _with_completion(self):
value = self._provider_options(
TEXT_DOCUMENT_COMPLETION, default=CompletionOptions()
@@ -333,10 +349,12 @@ def _with_semantic_tokens(self):
self.server_cap.semantic_tokens_provider = value
return self
+ full_support: Union[bool, SemanticTokensOptionsFullType1] = (
+ TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL in self.features
+ )
+
if TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA in self.features:
full_support = SemanticTokensOptionsFullType1(delta=True)
- else:
- full_support = TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL in self.features
options = SemanticTokensOptions(
legend=value,
@@ -416,7 +434,8 @@ def _build(self):
def build(self):
return (
- self._with_text_doc_sync()
+ self._with_text_document_sync()
+ ._with_notebook_document_sync()
._with_completion()
._with_hover()
._with_signature_help()
diff --git a/pygls/exceptions.py b/pygls/exceptions.py
index 533640e6..33fe2946 100644
--- a/pygls/exceptions.py
+++ b/pygls/exceptions.py
@@ -17,6 +17,8 @@
# limitations under the License. #
############################################################################
import traceback
+from typing import Set
+from typing import Type
class JsonRpcException(Exception):
@@ -65,7 +67,7 @@ def to_dict(self):
class JsonRpcInternalError(JsonRpcException):
- CODE = -32602
+ CODE = -32603
MESSAGE = "Internal Error"
@classmethod
@@ -158,7 +160,7 @@ def _is_server_error_code(code):
return -32099 <= code <= -32000
-_EXCEPTIONS = (
+_EXCEPTIONS: Set[Type[JsonRpcException]] = {
JsonRpcInternalError,
JsonRpcInvalidParams,
JsonRpcInvalidRequest,
@@ -166,7 +168,7 @@ def _is_server_error_code(code):
JsonRpcParseError,
JsonRpcRequestCancelled,
JsonRpcServerError,
-)
+}
class PyglsError(Exception):
diff --git a/pygls/protocol.py b/pygls/protocol.py
index 28b7d907..b15757b0 100644
--- a/pygls/protocol.py
+++ b/pygls/protocol.py
@@ -54,6 +54,9 @@
INITIALIZE,
INITIALIZED,
METHOD_TO_TYPES,
+ NOTEBOOK_DOCUMENT_DID_CHANGE,
+ NOTEBOOK_DOCUMENT_DID_CLOSE,
+ NOTEBOOK_DOCUMENT_DID_OPEN,
LOG_TRACE,
SET_TRACE,
SHUTDOWN,
@@ -74,9 +77,12 @@
from lsprotocol.types import (
ApplyWorkspaceEditParams,
Diagnostic,
+ DidChangeNotebookDocumentParams,
DidChangeTextDocumentParams,
DidChangeWorkspaceFoldersParams,
+ DidCloseNotebookDocumentParams,
DidCloseTextDocumentParams,
+ DidOpenNotebookDocumentParams,
DidOpenTextDocumentParams,
ExecuteCommandParams,
InitializeParams,
@@ -497,16 +503,16 @@ def _send_data(self, data):
body = json.dumps(data, default=self._serialize_message)
logger.info("Sending data: %s", body)
- body = body.encode(self.CHARSET)
- if not self._send_only_body:
- header = (
- f"Content-Length: {len(body)}\r\n"
- f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
- ).encode(self.CHARSET)
+ if self._send_only_body:
+ self.transport.write(body)
+ return
- self.transport.write(header + body)
- else:
- self.transport.write(body.decode("utf-8"))
+ header = (
+ f"Content-Length: {len(body)}\r\n"
+ f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
+ ).encode(self.CHARSET)
+
+ self.transport.write(header + body.encode(self.CHARSET))
except Exception as error:
logger.exception("Error sending data", exc_info=True)
self._server._report_server_error(error, JsonRpcInternalError)
@@ -625,7 +631,7 @@ def send_request(self, method, params=None, callback=None, msg_id=None):
jsonrpc=JsonRPCProtocol.VERSION,
)
- future = Future()
+ future = Future() # type: ignore[var-annotated]
# If callback function is given, call it when result is received
if callback:
@@ -704,7 +710,7 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
def __init__(self, server, converter):
super().__init__(server, converter)
- self.workspace = None
+ self._workspace: Optional[Workspace] = None
self.trace = None
from pygls.progress import Progress
@@ -721,10 +727,22 @@ def __init__(self, server, converter):
def _register_builtin_features(self):
"""Registers generic LSP features from this class."""
for name in dir(self):
+ if name in {"workspace"}:
+ continue
+
attr = getattr(self, name)
if callable(attr) and hasattr(attr, "method_name"):
self.fm.add_builtin_feature(attr.method_name, attr)
+ @property
+ def workspace(self) -> Workspace:
+ if self._workspace is None:
+ raise RuntimeError(
+ "The workspace is not available - has the server been initialized?"
+ )
+
+ return self._workspace
+
@lru_cache()
def get_message_type(self, method: str) -> Optional[Type]:
"""Return LSP type definitions, as provided by `lsprotocol`"""
@@ -768,14 +786,18 @@ def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
self._server.process_id = params.process_id
+ text_document_sync_kind = self._server._text_document_sync_kind
+ notebook_document_sync = self._server._notebook_document_sync
+
# Initialize server capabilities
self.client_capabilities = params.capabilities
self.server_capabilities = ServerCapabilitiesBuilder(
self.client_capabilities,
- {**self.fm.features, **self.fm.builtin_features}.keys(),
+ set({**self.fm.features, **self.fm.builtin_features}.keys()),
self.fm.feature_options,
list(self.fm.commands.keys()),
- self._server.sync_kind,
+ text_document_sync_kind,
+ notebook_document_sync,
).build()
logger.debug(
"Server capabilities: %s",
@@ -787,7 +809,9 @@ def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
# Initialize the workspace
workspace_folders = params.workspace_folders or []
- self.workspace = Workspace(root_uri, self._server.sync_kind, workspace_folders)
+ self._workspace = Workspace(
+ root_uri, text_document_sync_kind, workspace_folders
+ )
self.trace = TraceValues.Off
@@ -818,17 +842,38 @@ def lsp_text_document__did_change(
(Incremental(from server capabilities); not configurable for now)
"""
for change in params.content_changes:
- self.workspace.update_document(params.text_document, change)
+ self.workspace.update_text_document(params.text_document, change)
@lsp_method(TEXT_DOCUMENT_DID_CLOSE)
def lsp_text_document__did_close(self, params: DidCloseTextDocumentParams) -> None:
"""Removes document from workspace."""
- self.workspace.remove_document(params.text_document.uri)
+ self.workspace.remove_text_document(params.text_document.uri)
@lsp_method(TEXT_DOCUMENT_DID_OPEN)
def lsp_text_document__did_open(self, params: DidOpenTextDocumentParams) -> None:
"""Puts document to the workspace."""
- self.workspace.put_document(params.text_document)
+ self.workspace.put_text_document(params.text_document)
+
+ @lsp_method(NOTEBOOK_DOCUMENT_DID_OPEN)
+ def lsp_notebook_document__did_open(
+ self, params: DidOpenNotebookDocumentParams
+ ) -> None:
+ """Put a notebook document into the workspace"""
+ self.workspace.put_notebook_document(params)
+
+ @lsp_method(NOTEBOOK_DOCUMENT_DID_CHANGE)
+ def lsp_notebook_document__did_change(
+ self, params: DidChangeNotebookDocumentParams
+ ) -> None:
+ """Update a notebook's contents"""
+ self.workspace.update_notebook_document(params)
+
+ @lsp_method(NOTEBOOK_DOCUMENT_DID_CLOSE)
+ def lsp_notebook_document__did_close(
+ self, params: DidCloseNotebookDocumentParams
+ ) -> None:
+ """Remove a notebook document from the workspace."""
+ self.workspace.remove_notebook_document(params)
@lsp_method(SET_TRACE)
def lsp_set_trace(self, params: SetTraceParams) -> None:
@@ -958,10 +1003,24 @@ def publish_diagnostics(
version: Optional[int] = None,
**kwargs,
):
- """
- Sends diagnostic notification to the client.
- Deprecation:
- `uri`, `diagnostics` and `version` fields will be deprecated
+ """Sends diagnostic notification to the client.
+
+ .. deprecated:: 1.0.1
+
+ Passing ``(uri, diagnostics, version)`` as arguments is deprecated.
+ Pass an instance of :class:`~lsprotocol.types.PublishDiagnosticParams`
+ instead.
+
+ Parameters
+ ----------
+ params_or_uri
+ The :class:`~lsprotocol.types.PublishDiagnosticParams` to send to the client.
+
+ diagnostics
+ *Deprecated*. The diagnostics to publish
+
+ version
+ *Deprecated*: The version number
"""
params = self._publish_diagnostics_deprecator(
params_or_uri, diagnostics, version, **kwargs
diff --git a/pygls/server.py b/pygls/server.py
index 4993ad87..b26c9a4b 100644
--- a/pygls/server.py
+++ b/pygls/server.py
@@ -21,8 +21,18 @@
import sys
from concurrent.futures import Future, ThreadPoolExecutor
from threading import Event
-from typing import Any, Callable, List, Optional, TextIO, TypeVar, Union
+from typing import (
+ Any,
+ Callable,
+ List,
+ Optional,
+ TextIO,
+ Type,
+ TypeVar,
+ Union,
+)
+import cattrs
from pygls import IS_PYODIDE
from pygls.lsp import ConfigCallbackType, ShowDocumentCallbackType
from pygls.exceptions import PyglsError, JsonRpcException, FeatureRequestError
@@ -30,6 +40,7 @@
ClientCapabilities,
Diagnostic,
MessageType,
+ NotebookDocumentSyncOptions,
RegistrationParams,
ServerCapabilities,
ShowDocumentParams,
@@ -40,7 +51,7 @@
WorkspaceConfigurationParams,
)
from pygls.progress import Progress
-from pygls.protocol import LanguageServerProtocol, default_converter
+from pygls.protocol import JsonRPCProtocol, LanguageServerProtocol, default_converter
from pygls.workspace import Workspace
if not IS_PYODIDE:
@@ -147,52 +158,43 @@ def write(self, data: Any) -> None:
class Server:
- """Class that represents async server. It can be started using TCP or IO.
-
- Args:
- protocol_cls(Protocol): Protocol implementation that must be derived
- from `asyncio.Protocol`
+ """Base server class
- converter_factory: Factory function to use when constructing a cattrs converter.
+ Parameters
+ ----------
+ protocol_cls
+ Protocol implementation that must be derive from :class:`~pygls.protocol.JsonRPCProtocol`
- loop(AbstractEventLoop): asyncio event loop
+ converter_factory
+ Factory function to use when constructing a cattrs converter.
- max_workers(int, optional): Number of workers for `ThreadPool` and
- `ThreadPoolExecutor`
+ loop
+ The asyncio event loop
- sync_kind(TextDocumentSyncKind): Text document synchronization option
- - None(0): no synchronization
- - Full(1): replace whole text
- - Incremental(2): replace text within a given range
+ max_workers
+ Maximum number of workers for `ThreadPool` and `ThreadPoolExecutor`
- Attributes:
- _max_workers(int): Number of workers for thread pool executor
- _server(Server): Server object which can be used to stop the process
- _stop_event(Event): Event used for stopping `aio_readline`
- _thread_pool(ThreadPool): Thread pool for executing methods decorated
- with `@ls.thread()` - lazy instantiated
- _thread_pool_executor(ThreadPoolExecutor): Thread pool executor
- passed to `run_in_executor`
- - lazy instantiated
"""
def __init__(
self,
- protocol_cls,
- converter_factory,
- loop=None,
- max_workers=2,
- sync_kind=TextDocumentSyncKind.Incremental,
+ protocol_cls: Type[JsonRPCProtocol],
+ converter_factory: Callable[[], cattrs.Converter],
+ loop: Optional[asyncio.AbstractEventLoop] = None,
+ max_workers: int = 2,
+ sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
):
if not issubclass(protocol_cls, asyncio.Protocol):
raise TypeError("Protocol class should be subclass of asyncio.Protocol")
self._max_workers = max_workers
self._server = None
- self._stop_event = None
- self._thread_pool = None
- self._thread_pool_executor = None
- self.sync_kind = sync_kind
+ self._stop_event: Optional[Event] = None
+ self._thread_pool: Optional[ThreadPool] = None
+ self._thread_pool_executor: Optional[ThreadPoolExecutor] = None
+
+ if sync_kind is not None:
+ self.text_document_sync_kind = sync_kind
if loop is None:
loop = asyncio.new_event_loop()
@@ -208,7 +210,8 @@ def shutdown(self):
"""Shutdown server."""
logger.info("Shutting down the server")
- self._stop_event.set()
+ if self._stop_event is not None:
+ self._stop_event.set()
if self._thread_pool:
self._thread_pool.terminate()
@@ -221,7 +224,7 @@ def shutdown(self):
self._server.close()
self.loop.run_until_complete(self._server.wait_closed())
- if self._owns_loop and not self.loop.is_closed:
+ if self._owns_loop and not self.loop.is_closed():
logger.info("Closing the event loop.")
self.loop.close()
@@ -233,7 +236,7 @@ def start_io(self, stdin: Optional[TextIO] = None, stdout: Optional[TextIO] = No
transport = StdOutTransportAdapter(
stdin or sys.stdin.buffer, stdout or sys.stdout.buffer
)
- self.lsp.connection_made(transport)
+ self.lsp.connection_made(transport) # type: ignore[arg-type]
try:
self.loop.run_until_complete(
@@ -258,7 +261,7 @@ def start_pyodide(self):
# Note: We don't actually start anything running as the main event
# loop will be handled by the web platform.
transport = PyodideTransportAdapter(sys.stdout)
- self.lsp.connection_made(transport)
+ self.lsp.connection_made(transport) # type: ignore[arg-type]
self.lsp._send_only_body = True # Don't send headers within the payload
def start_tcp(self, host: str, port: int) -> None:
@@ -266,7 +269,7 @@ def start_tcp(self, host: str, port: int) -> None:
logger.info("Starting TCP server on %s:%s", host, port)
self._stop_event = Event()
- self._server = self.loop.run_until_complete(
+ self._server = self.loop.run_until_complete( # type: ignore[assignment]
self.loop.create_server(self.lsp, host, port)
)
try:
@@ -298,7 +301,7 @@ async def connection_made(websocket, _):
)
start_server = serve(connection_made, host, port, loop=self.loop)
- self._server = start_server.ws_server
+ self._server = start_server.ws_server # type: ignore[assignment]
self.loop.run_until_complete(start_server)
try:
@@ -331,17 +334,44 @@ def thread_pool_executor(self) -> ThreadPoolExecutor:
class LanguageServer(Server):
- """A class that represents Language server using Language Server Protocol.
+ """The default LanguageServer
This class can be extended and it can be passed as a first argument to
registered commands/features.
- Args:
- name(str): Name of the server
- version(str): Version of the server
- protocol_cls(LanguageServerProtocol): LSP or any subclass of it
- max_workers(int, optional): Number of workers for `ThreadPool` and
- `ThreadPoolExecutor`
+ .. |ServerInfo| replace:: :class:`~lsprotocol.types.InitializeResultServerInfoType`
+
+ Parameters
+ ----------
+ name
+ Name of the server, used to populate |ServerInfo| which is sent to
+ the client during initialization
+
+ version
+ Version of the server, used to populate |ServerInfo| which is sent to
+ the client during initialization
+
+ protocol_cls
+ The :class:`~pygls.protocol.LanguageServerProtocol` class definition, or any
+ subclass of it.
+
+ max_workers
+ Maximum number of workers for ``ThreadPool`` and ``ThreadPoolExecutor``
+
+ text_document_sync_kind
+ Text document synchronization method
+
+ None
+ No synchronization
+
+ :attr:`~lsprotocol.types.TextDocumentSyncKind.Full`
+ Send entire document text with each update
+
+ :attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`
+ Send only the region of text that changed with each update
+
+ notebook_document_sync
+ Advertise :lsp:`NotebookDocument` support to the client.
"""
lsp: LanguageServerProtocol
@@ -359,8 +389,10 @@ def __init__(
name: str,
version: str,
loop=None,
- protocol_cls=LanguageServerProtocol,
+ protocol_cls: Type[LanguageServerProtocol] = LanguageServerProtocol,
converter_factory=default_converter,
+ text_document_sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
+ notebook_document_sync: Optional[NotebookDocumentSyncOptions] = None,
max_workers: int = 2,
):
if not issubclass(protocol_cls, LanguageServerProtocol):
@@ -370,6 +402,8 @@ def __init__(
self.name = name
self.version = version
+ self._text_document_sync_kind = text_document_sync_kind
+ self._notebook_document_sync = notebook_document_sync
super().__init__(protocol_cls, converter_factory, loop, max_workers)
def apply_edit(
@@ -387,16 +421,19 @@ def apply_edit_async(
def command(self, command_name: str) -> Callable[[F], F]:
"""Decorator used to register custom commands.
- Example:
- @ls.command('myCustomCommand')
- def my_cmd(ls, a, b, c):
- pass
+ Example
+ -------
+ ::
+
+ @ls.command('myCustomCommand')
+ def my_cmd(ls, a, b, c):
+ pass
"""
return self.lsp.fm.command(command_name)
@property
def client_capabilities(self) -> ClientCapabilities:
- """Return client capabilities."""
+ """The client's capabilities."""
return self.lsp.client_capabilities
def feature(
@@ -406,10 +443,13 @@ def feature(
) -> Callable[[F], F]:
"""Decorator used to register LSP features.
- Example:
- @ls.feature('textDocument/completion', CompletionOptions(trigger_characters=['.']))
- def completions(ls, params: CompletionParams):
- return CompletionList(is_incomplete=False, items=[CompletionItem("Completion 1")])
+ Example
+ -------
+ ::
+
+ @ls.feature('textDocument/completion', CompletionOptions(trigger_characters=['.']))
+ def completions(ls, params: CompletionParams):
+ return CompletionList(is_incomplete=False, items=[CompletionItem("Completion 1")])
"""
return self.lsp.fm.feature(feature_name, options)
diff --git a/pygls/workspace.py b/pygls/workspace.py
index 4fcdc4f4..fad94ef6 100644
--- a/pygls/workspace.py
+++ b/pygls/workspace.py
@@ -16,29 +16,23 @@
# See the License for the specific language governing permissions and #
# limitations under the License. #
############################################################################
+import copy
import io
import logging
import os
import re
-from typing import List, Optional, Pattern
-
-from lsprotocol.types import (
- Position,
- Range,
- TextDocumentContentChangeEvent,
- TextDocumentContentChangeEvent_Type1,
- TextDocumentItem,
- TextDocumentSyncKind,
- VersionedTextDocumentIdentifier,
- WorkspaceFolder,
-)
+import warnings
+from typing import Dict, List, Optional, Pattern
+
+from lsprotocol import types
+
from pygls.uris import to_fs_path, uri_scheme
# TODO: this is not the best e.g. we capture numbers
RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
RE_START_WORD = re.compile("[A-Za-z_0-9]*$")
-log = logging.getLogger(__name__)
+logger = logging.getLogger(__name__)
def is_char_beyond_multilingual_plane(char: str) -> bool:
@@ -63,7 +57,7 @@ def utf16_num_units(chars: str):
return len(chars) + utf16_unit_offset(chars)
-def position_from_utf16(lines: List[str], position: Position) -> Position:
+def position_from_utf16(lines: List[str], position: types.Position) -> types.Position:
"""Convert the position.character from utf-16 code units to utf-32.
A python application can't use the character member of `Position`
@@ -89,9 +83,9 @@ def position_from_utf16(lines: List[str], position: Position) -> Position:
The position with `character` being converted to utf-32 code units.
"""
if len(lines) == 0:
- return Position(0, 0)
+ return types.Position(0, 0)
if position.line >= len(lines):
- return Position(len(lines) - 1, utf16_num_units(lines[-1]))
+ return types.Position(len(lines) - 1, utf16_num_units(lines[-1]))
_line = lines[position.line]
_line = _line.replace("\r\n", "\n") # TODO: it's a bit of a hack
@@ -99,7 +93,7 @@ def position_from_utf16(lines: List[str], position: Position) -> Position:
_utf32_len = len(_line)
if _utf16_len == 0:
- return Position(position.line, 0)
+ return types.Position(position.line, 0)
_utf16_end_of_line = utf16_num_units(_line)
if position.character > _utf16_end_of_line:
@@ -124,11 +118,11 @@ def position_from_utf16(lines: List[str], position: Position) -> Position:
_utf16_index += 1
utf32_index += 1
- position = Position(line=position.line, character=utf32_index)
+ position = types.Position(line=position.line, character=utf32_index)
return position
-def position_to_utf16(lines: List[str], position: Position) -> Position:
+def position_to_utf16(lines: List[str], position: types.Position) -> types.Position:
"""Convert the position.character from utf-32 to utf-16 code units.
A python application can't use the character member of `Position`
@@ -154,16 +148,16 @@ def position_to_utf16(lines: List[str], position: Position) -> Position:
The position with `character` being converted to utf-16 code units.
"""
try:
- return Position(
+ return types.Position(
line=position.line,
character=position.character
+ utf16_unit_offset(lines[position.line][: position.character]),
)
except IndexError:
- return Position(line=len(lines), character=0)
+ return types.Position(line=len(lines), character=0)
-def range_from_utf16(lines: List[str], range: Range) -> Range:
+def range_from_utf16(lines: List[str], range: types.Range) -> types.Range:
"""Convert range.[start|end].character from utf-16 code units to utf-32.
Arguments:
@@ -175,14 +169,14 @@ def range_from_utf16(lines: List[str], range: Range) -> Range:
Returns:
The range with `character` offsets being converted to utf-16 code units.
"""
- range_new = Range(
+ range_new = types.Range(
start=position_from_utf16(lines, range.start),
end=position_from_utf16(lines, range.end),
)
return range_new
-def range_to_utf16(lines: List[str], range: Range) -> Range:
+def range_to_utf16(lines: List[str], range: types.Range) -> types.Range:
"""Convert range.[start|end].character from utf-32 to utf-16 code units.
Arguments:
@@ -194,13 +188,13 @@ def range_to_utf16(lines: List[str], range: Range) -> Range:
Returns:
The range with `character` offsets being converted to utf-32 code units.
"""
- return Range(
+ return types.Range(
start=position_to_utf16(lines, range.start),
end=position_to_utf16(lines, range.end),
)
-class Document(object):
+class TextDocument(object):
def __init__(
self,
uri: str,
@@ -208,7 +202,7 @@ def __init__(
version: Optional[int] = None,
language_id: Optional[str] = None,
local: bool = True,
- sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
+ sync_kind: types.TextDocumentSyncKind = types.TextDocumentSyncKind.Incremental,
):
self.uri = uri
self.version = version
@@ -219,15 +213,17 @@ def __init__(
self._local = local
self._source = source
- self._is_sync_kind_full = sync_kind == TextDocumentSyncKind.Full
- self._is_sync_kind_incremental = sync_kind == TextDocumentSyncKind.Incremental
- self._is_sync_kind_none = sync_kind == TextDocumentSyncKind.None_
+ self._is_sync_kind_full = sync_kind == types.TextDocumentSyncKind.Full
+ self._is_sync_kind_incremental = (
+ sync_kind == types.TextDocumentSyncKind.Incremental
+ )
+ self._is_sync_kind_none = sync_kind == types.TextDocumentSyncKind.None_
def __str__(self):
return str(self.uri)
def _apply_incremental_change(
- self, change: TextDocumentContentChangeEvent_Type1
+ self, change: types.TextDocumentContentChangeEvent_Type1
) -> None:
"""Apply an ``Incremental`` text change to the document"""
lines = self.lines
@@ -268,32 +264,35 @@ def _apply_incremental_change(
self._source = new.getvalue()
- def _apply_full_change(self, change: TextDocumentContentChangeEvent) -> None:
+ def _apply_full_change(self, change: types.TextDocumentContentChangeEvent) -> None:
"""Apply a ``Full`` text change to the document."""
self._source = change.text
- def _apply_none_change(self, change: TextDocumentContentChangeEvent) -> None:
+ def _apply_none_change(self, change: types.TextDocumentContentChangeEvent) -> None:
"""Apply a ``None`` text change to the document
Currently does nothing, provided for consistency.
"""
pass
- def apply_change(self, change: TextDocumentContentChangeEvent) -> None:
+ def apply_change(self, change: types.TextDocumentContentChangeEvent) -> None:
"""Apply a text change to a document, considering TextDocumentSyncKind
- Performs either ``Incremental``, ``Full``, or ``None`` synchronization based on
- both the Client request and server capabilities.
+ Performs either
+ :attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`,
+ :attr:`~lsprotocol.types.TextDocumentSyncKind.Full`, or no synchronization
+ based on both the client request and server capabilities.
+
+ .. admonition:: ``Incremental`` versus ``Full`` synchronization
- ``Incremental`` versus ``Full`` synchronization:
- Even if a server accepts ``Incremantal`` SyncKinds, clients may request
- a ``Full`` SyncKind. In LSP 3.x, clients make this request by omitting
- both Range and RangeLength from their request. Consequently, the
- attributes "range" and "rangeLength" will be missing from ``Full``
- content update client requests in the pygls Python library.
+ Even if a server accepts ``Incremantal`` SyncKinds, clients may request
+ a ``Full`` SyncKind. In LSP 3.x, clients make this request by omitting
+ both Range and RangeLength from their request. Consequently, the
+ attributes "range" and "rangeLength" will be missing from ``Full``
+ content update client requests in the pygls Python library.
"""
- if isinstance(change, TextDocumentContentChangeEvent_Type1):
+ if isinstance(change, types.TextDocumentContentChangeEvent_Type1):
if self._is_sync_kind_incremental:
self._apply_incremental_change(change)
return
@@ -301,7 +300,7 @@ def apply_change(self, change: TextDocumentContentChangeEvent) -> None:
# assumptions in test_document/test_document_full_edit. Test breaks
# otherwise, and fixing the tests would require a broader fix to
# protocol.py.
- log.error(
+ logger.error(
"Unsupported client-provided TextDocumentContentChangeEvent. "
"Please update / submit a Pull Request to your LSP client."
)
@@ -315,7 +314,7 @@ def apply_change(self, change: TextDocumentContentChangeEvent) -> None:
def lines(self) -> List[str]:
return self.source.splitlines(True)
- def offset_at_position(self, position: Position) -> int:
+ def offset_at_position(self, position: types.Position) -> int:
"""Return the character offset pointed at by the given position."""
lines = self.lines
pos = position_from_utf16(lines, position)
@@ -331,28 +330,38 @@ def source(self) -> str:
def word_at_position(
self,
- position: Position,
+ position: types.Position,
re_start_word: Pattern = RE_START_WORD,
re_end_word: Pattern = RE_END_WORD,
) -> str:
"""Return the word at position.
- Arguments:
- position (Position):
- The line and character offset.
- re_start_word (Pattern):
- The regular expression for extracting the word backward from
- position. Specifically, the first match from a re.findall
- call on the line up to the character value of position. The
- default pattern is '[A-Za-z_0-9]*$'.
- re_end_word (Pattern):
- The regular expression for extracting the word forward from
- position. Specifically, the last match from a re.findall
- call on the line from the character value of position. The
- default pattern is '^[A-Za-z_0-9]*'.
-
- Returns:
- The word (obtained by concatenating the two matches) at position.
+ The word is constructed in two halves, the first half is found by taking
+ the first match of ``re_start_word`` on the line up until
+ ``position.character``.
+
+ The second half is found by taking ``position.character`` up until the
+ last match of ``re_end_word`` on the line.
+
+ :func:`python:re.findall` is used to find the matches.
+
+ Parameters
+ ----------
+ position
+ The line and character offset.
+
+ re_start_word
+ The regular expression for extracting the word backward from
+ position. The default pattern is ``[A-Za-z_0-9]*$``.
+
+ re_end_word
+ The regular expression for extracting the word forward from
+ position. The default pattern is ``^[A-Za-z_0-9]*``.
+
+ Returns
+ -------
+ str
+ The word (obtained by concatenating the two matches) at position.
"""
lines = self.lines
if position.line >= len(lines):
@@ -373,6 +382,10 @@ def word_at_position(
return m_start[0] + m_end[-1]
+# For backwards compatibility
+Document = TextDocument
+
+
class Workspace(object):
def __init__(self, root_uri, sync_kind=None, workspace_folders=None):
self._root_uri = root_uri
@@ -380,20 +393,24 @@ def __init__(self, root_uri, sync_kind=None, workspace_folders=None):
self._root_path = to_fs_path(self._root_uri)
self._sync_kind = sync_kind
self._folders = {}
- self._docs = {}
+ self._text_documents: Dict[str, TextDocument] = {}
+ self._notebook_documents: Dict[str, types.NotebookDocument] = {}
+
+ # Used to lookup notebooks which contain a given cell.
+ self._cell_in_notebook: Dict[str, str] = {}
if workspace_folders is not None:
for folder in workspace_folders:
self.add_folder(folder)
- def _create_document(
+ def _create_text_document(
self,
doc_uri: str,
source: Optional[str] = None,
version: Optional[int] = None,
language_id: Optional[str] = None,
- ) -> Document:
- return Document(
+ ) -> TextDocument:
+ return TextDocument(
doc_uri,
source=source,
version=version,
@@ -401,43 +418,126 @@ def _create_document(
sync_kind=self._sync_kind,
)
- def add_folder(self, folder: WorkspaceFolder):
+ def add_folder(self, folder: types.WorkspaceFolder):
self._folders[folder.uri] = folder
@property
def documents(self):
- return self._docs
+ warnings.warn(
+ "'workspace.documents' has been deprecated, use "
+ "'workspace.text_documents' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.text_documents
+
+ @property
+ def notebook_documents(self):
+ return self._notebook_documents
+
+ @property
+ def text_documents(self):
+ return self._text_documents
@property
def folders(self):
return self._folders
- def get_document(self, doc_uri: str) -> Document:
+ def get_notebook_document(
+ self, *, notebook_uri: Optional[str] = None, cell_uri: Optional[str] = None
+ ) -> Optional[types.NotebookDocument]:
+ """Return the notebook corresponding with the given uri.
+
+ If both ``notebook_uri`` and ``cell_uri`` are given, ``notebook_uri`` takes
+ precedence.
+
+ Parameters
+ ----------
+ notebook_uri
+ If given, return the notebook document with the given uri.
+
+ cell_uri
+ If given, return the notebook document which contains a cell with the
+ given uri
+
+ Returns
+ -------
+ Optional[NotebookDocument]
+ The requested notebook document if found, ``None`` otherwise.
+ """
+ if notebook_uri is not None:
+ return self._notebook_documents.get(notebook_uri)
+
+ if cell_uri is not None:
+ notebook_uri = self._cell_in_notebook.get(cell_uri)
+ if notebook_uri is None:
+ return None
+
+ return self._notebook_documents.get(notebook_uri)
+
+ return None
+
+ def get_text_document(self, doc_uri: str) -> TextDocument:
"""
Return a managed document if-present,
else create one pointing at disk.
See https://github.com/Microsoft/language-server-protocol/issues/177
"""
- return self._docs.get(doc_uri) or self._create_document(doc_uri)
+ return self._text_documents.get(doc_uri) or self._create_text_document(doc_uri)
def is_local(self):
return (
self._root_uri_scheme == "" or self._root_uri_scheme == "file"
) and os.path.exists(self._root_path)
- def put_document(self, text_document: TextDocumentItem):
+ def put_notebook_document(self, params: types.DidOpenNotebookDocumentParams):
+ notebook = params.notebook_document
+
+ # Create a fresh instance to ensure our copy cannot be accidentally modified.
+ self._notebook_documents[notebook.uri] = copy.deepcopy(notebook)
+
+ for cell_document in params.cell_text_documents:
+ self.put_text_document(cell_document, notebook_uri=notebook.uri)
+
+ def put_text_document(
+ self,
+ text_document: types.TextDocumentItem,
+ notebook_uri: Optional[str] = None,
+ ):
+ """Add a text document to the workspace.
+
+ Parameters
+ ----------
+ text_document
+ The text document to add
+
+ notebook_uri
+ If set, indicates that this text document represents a cell in a notebook
+ document
+ """
doc_uri = text_document.uri
- self._docs[doc_uri] = self._create_document(
+ self._text_documents[doc_uri] = self._create_text_document(
doc_uri,
source=text_document.text,
version=text_document.version,
language_id=text_document.language_id,
)
- def remove_document(self, doc_uri: str):
- self._docs.pop(doc_uri)
+ if notebook_uri:
+ self._cell_in_notebook[doc_uri] = notebook_uri
+
+ def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
+ notebook_uri = params.notebook_document.uri
+ self._notebook_documents.pop(notebook_uri, None)
+
+ for cell_document in params.cell_text_documents:
+ self.remove_text_document(cell_document.uri)
+
+ def remove_text_document(self, doc_uri: str):
+ self._text_documents.pop(doc_uri, None)
+ self._cell_in_notebook.pop(doc_uri, None)
def remove_folder(self, folder_uri: str):
self._folders.pop(folder_uri, None)
@@ -454,11 +554,95 @@ def root_path(self):
def root_uri(self):
return self._root_uri
- def update_document(
+ def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
+ uri = params.notebook_document.uri
+ notebook = self._notebook_documents[uri]
+ notebook.version = params.notebook_document.version
+
+ if params.change.metadata:
+ notebook.metadata = params.change.metadata
+
+ cell_changes = params.change.cells
+ if cell_changes is None:
+ return
+
+ # Process changes to any cell metadata.
+ nb_cells = {cell.document: cell for cell in notebook.cells}
+ for new_data in cell_changes.data or []:
+ nb_cell = nb_cells.get(new_data.document)
+ if nb_cell is None:
+ logger.warning(
+ "Ignoring metadata for '%s': not in notebook.", new_data.document
+ )
+ continue
+
+ nb_cell.kind = new_data.kind
+ nb_cell.metadata = new_data.metadata
+ nb_cell.execution_summary = new_data.execution_summary
+
+ # Process changes to the notebook's structure
+ structure = cell_changes.structure
+ if structure:
+ cells = notebook.cells
+ new_cells = structure.array.cells or []
+
+ # Re-order the cells
+ before = cells[: structure.array.start]
+ after = cells[(structure.array.start + structure.array.delete_count) :]
+ notebook.cells = [*before, *new_cells, *after]
+
+ for new_cell in structure.did_open or []:
+ self.put_text_document(new_cell, notebook_uri=uri)
+
+ for removed_cell in structure.did_close or []:
+ self.remove_text_document(removed_cell.uri)
+
+ # Process changes to the text content of existing cells.
+ for text in cell_changes.text_content or []:
+ for change in text.changes:
+ self.update_text_document(text.document, change)
+
+ def update_text_document(
self,
- text_doc: VersionedTextDocumentIdentifier,
- change: TextDocumentContentChangeEvent,
+ text_doc: types.VersionedTextDocumentIdentifier,
+ change: types.TextDocumentContentChangeEvent,
):
doc_uri = text_doc.uri
- self._docs[doc_uri].apply_change(change)
- self._docs[doc_uri].version = text_doc.version
+ self._text_documents[doc_uri].apply_change(change)
+ self._text_documents[doc_uri].version = text_doc.version
+
+ def get_document(self, *args, **kwargs):
+ warnings.warn(
+ "'workspace.get_document' has been deprecated, use "
+ "'workspace.get_text_document' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.get_text_document(*args, **kwargs)
+
+ def remove_document(self, *args, **kwargs):
+ warnings.warn(
+ "'workspace.remove_document' has been deprecated, use "
+ "'workspace.remove_text_document' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.remove_text_document(*args, **kwargs)
+
+ def put_document(self, *args, **kwargs):
+ warnings.warn(
+ "'workspace.put_document' has been deprecated, use "
+ "'workspace.put_text_document' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.put_text_document(*args, **kwargs)
+
+ def update_document(self, *args, **kwargs):
+ warnings.warn(
+ "'workspace.update_document' has been deprecated, use "
+ "'workspace.update_text_document' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.update_text_document(*args, **kwargs)
diff --git a/pyproject.toml b/pyproject.toml
index fa323a6e..93e4cfab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -73,6 +73,9 @@ line-length = 120
line-length = 88
extend-exclude = "pygls/lsp/client.py"
+[tool.mypy]
+check_untyped_defs = true
+
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
diff --git a/tests/conftest.py b/tests/conftest.py
index 1925e9d8..c816bd13 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -21,6 +21,7 @@
import sys
import pytest
+from lsprotocol import types
from pygls import uris, IS_PYODIDE, IS_WIN
from pygls.feature_manager import FeatureManager
@@ -123,4 +124,7 @@ def feature_manager():
@pytest.fixture
def workspace(tmpdir):
"""Return a workspace."""
- return Workspace(uris.from_fs_path(str(tmpdir)))
+ return Workspace(
+ uris.from_fs_path(str(tmpdir)),
+ sync_kind=types.TextDocumentSyncKind.Incremental,
+ )
diff --git a/tests/test_feature_manager.py b/tests/test_feature_manager.py
index 24856c7f..d9655b85 100644
--- a/tests/test_feature_manager.py
+++ b/tests/test_feature_manager.py
@@ -616,6 +616,7 @@ def _():
feature_manager.feature_options,
[],
None,
+ None,
).build()
assert expected == actual
@@ -642,6 +643,7 @@ def _():
feature_manager.feature_options,
[],
None,
+ None,
).build()
assert expected == actual
diff --git a/tests/test_workspace.py b/tests/test_workspace.py
index 5f9f09a4..53c5a52d 100644
--- a/tests/test_workspace.py
+++ b/tests/test_workspace.py
@@ -18,59 +18,416 @@
############################################################################
import os
+import pytest
+from lsprotocol import types
+
from pygls import uris
-from lsprotocol.types import TextDocumentItem, WorkspaceFolder
from pygls.workspace import Workspace
DOC_URI = uris.from_fs_path(__file__)
DOC_TEXT = """test"""
-DOC = TextDocumentItem(uri=DOC_URI, language_id="plaintext", version=0, text=DOC_TEXT)
+DOC = types.TextDocumentItem(
+ uri=DOC_URI, language_id="plaintext", version=0, text=DOC_TEXT
+)
+NOTEBOOK = types.NotebookDocument(
+ uri="file:///path/to/notebook.ipynb",
+ notebook_type="jupyter-notebook",
+ version=0,
+ cells=[
+ types.NotebookCell(
+ kind=types.NotebookCellKind.Code,
+ document="nb-cell-scheme://path/to/notebook.ipynb#cv32321",
+ ),
+ types.NotebookCell(
+ kind=types.NotebookCellKind.Code,
+ document="nb-cell-scheme://path/to/notebook.ipynb#cp897h32",
+ ),
+ ],
+)
+NB_CELL_1 = types.TextDocumentItem(
+ uri="nb-cell-scheme://path/to/notebook.ipynb#cv32321",
+ language_id="python",
+ version=0,
+ text="# cell 1",
+)
+NB_CELL_2 = types.TextDocumentItem(
+ uri="nb-cell-scheme://path/to/notebook.ipynb#cp897h32",
+ language_id="python",
+ version=0,
+ text="# cell 2",
+)
+NB_CELL_3 = types.TextDocumentItem(
+ uri="nb-cell-scheme://path/to/notebook.ipynb#cq343eeds",
+ language_id="python",
+ version=0,
+ text="# cell 3",
+)
def test_add_folder(workspace):
dir_uri = os.path.dirname(DOC_URI)
dir_name = "test"
- workspace.add_folder(WorkspaceFolder(uri=dir_uri, name=dir_name))
+ workspace.add_folder(types.WorkspaceFolder(uri=dir_uri, name=dir_name))
assert workspace.folders[dir_uri].name == dir_name
-def test_get_document(workspace):
- workspace.put_document(DOC)
+def test_get_notebook_document_by_uri(workspace):
+ """Ensure that we can get a notebook given its uri."""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook == NOTEBOOK
+
+
+@pytest.mark.parametrize(
+ "cell,expected",
+ [
+ (NB_CELL_1, NOTEBOOK),
+ (NB_CELL_2, NOTEBOOK),
+ (NB_CELL_3, None),
+ (DOC, None),
+ ],
+)
+def test_get_notebook_document_by_cell_uri(workspace, cell, expected):
+ """Ensure that we can get a notebook given a uri of one of its cells"""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(cell_uri=cell.uri)
+ assert notebook == expected
+
- assert workspace.get_document(DOC_URI).source == DOC_TEXT
+def test_get_text_document(workspace):
+ workspace.put_text_document(DOC)
+
+ assert workspace.get_text_document(DOC_URI).source == DOC_TEXT
def test_get_missing_document(tmpdir, workspace):
doc_path = tmpdir.join("test_document.py")
doc_path.write(DOC_TEXT)
doc_uri = uris.from_fs_path(str(doc_path))
- assert workspace.get_document(doc_uri).source == DOC_TEXT
+ assert workspace.get_text_document(doc_uri).source == DOC_TEXT
+
+
+def test_put_notebook_document(workspace):
+ """Ensure that we can add notebook documents to the workspace correctly."""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+ assert NOTEBOOK.uri in workspace._notebook_documents
+ assert NB_CELL_1.uri in workspace._text_documents
+ assert NB_CELL_2.uri in workspace._text_documents
-def test_put_document(workspace):
- workspace.put_document(DOC)
- assert DOC_URI in workspace._docs
+
+def test_put_text_document(workspace):
+ workspace.put_text_document(DOC)
+ assert DOC_URI in workspace._text_documents
def test_remove_folder(workspace):
dir_uri = os.path.dirname(DOC_URI)
dir_name = "test"
- workspace.add_folder(WorkspaceFolder(uri=dir_uri, name=dir_name))
+ workspace.add_folder(types.WorkspaceFolder(uri=dir_uri, name=dir_name))
workspace.remove_folder(dir_uri)
assert dir_uri not in workspace.folders
-def test_remove_document(workspace):
- workspace.put_document(DOC)
- assert workspace.get_document(DOC_URI).source == DOC_TEXT
- workspace.remove_document(DOC_URI)
- assert workspace.get_document(DOC_URI)._source is None
+def test_remove_notebook_document(workspace):
+ """Ensure that we can correctly remove a document from the workspace."""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ assert NOTEBOOK.uri in workspace._notebook_documents
+ assert NB_CELL_1.uri in workspace._text_documents
+ assert NB_CELL_2.uri in workspace._text_documents
+
+ params = types.DidCloseNotebookDocumentParams(
+ notebook_document=types.NotebookDocumentIdentifier(uri=NOTEBOOK.uri),
+ cell_text_documents=[
+ types.TextDocumentIdentifier(uri=NB_CELL_1.uri),
+ types.TextDocumentIdentifier(uri=NB_CELL_2.uri),
+ ],
+ )
+ workspace.remove_notebook_document(params)
+
+ assert NOTEBOOK.uri not in workspace._notebook_documents
+ assert NB_CELL_1.uri not in workspace._text_documents
+ assert NB_CELL_2.uri not in workspace._text_documents
+
+
+def test_remove_text_document(workspace):
+ workspace.put_text_document(DOC)
+ assert workspace.get_text_document(DOC_URI).source == DOC_TEXT
+ workspace.remove_text_document(DOC_URI)
+ assert workspace.get_text_document(DOC_URI)._source is None
+
+
+def test_update_notebook_metadata(workspace):
+ """Ensure we can update a notebook's metadata correctly."""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 0
+ assert notebook.metadata is None
+
+ params = types.DidChangeNotebookDocumentParams(
+ notebook_document=types.VersionedNotebookDocumentIdentifier(
+ uri=NOTEBOOK.uri, version=31
+ ),
+ change=types.NotebookDocumentChangeEvent(
+ metadata={"custom": "metadata"},
+ ),
+ )
+ workspace.update_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 31
+ assert notebook.metadata == {"custom": "metadata"}
+
+
+def test_update_notebook_cell_data(workspace):
+ """Ensure we can update a notebook correctly when cell data changes."""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 0
+
+ cell_1 = notebook.cells[0]
+ assert cell_1.metadata is None
+ assert cell_1.execution_summary is None
+
+ cell_2 = notebook.cells[1]
+ assert cell_2.metadata is None
+ assert cell_2.execution_summary is None
+
+ params = types.DidChangeNotebookDocumentParams(
+ notebook_document=types.VersionedNotebookDocumentIdentifier(
+ uri=NOTEBOOK.uri, version=31
+ ),
+ change=types.NotebookDocumentChangeEvent(
+ cells=types.NotebookDocumentChangeEventCellsType(
+ data=[
+ types.NotebookCell(
+ kind=types.NotebookCellKind.Code,
+ document=NB_CELL_1.uri,
+ metadata={"slideshow": {"slide_type": "skip"}},
+ execution_summary=types.ExecutionSummary(
+ execution_order=2, success=True
+ ),
+ ),
+ types.NotebookCell(
+ kind=types.NotebookCellKind.Code,
+ document=NB_CELL_2.uri,
+ metadata={"slideshow": {"slide_type": "note"}},
+ execution_summary=types.ExecutionSummary(
+ execution_order=3, success=False
+ ),
+ ),
+ ]
+ )
+ ),
+ )
+ workspace.update_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 31
+
+ cell_1 = notebook.cells[0]
+ assert cell_1.metadata == {"slideshow": {"slide_type": "skip"}}
+ assert cell_1.execution_summary == types.ExecutionSummary(
+ execution_order=2, success=True
+ )
+
+ cell_2 = notebook.cells[1]
+ assert cell_2.metadata == {"slideshow": {"slide_type": "note"}}
+ assert cell_2.execution_summary == types.ExecutionSummary(
+ execution_order=3, success=False
+ )
+
+
+def test_update_notebook_cell_content(workspace):
+ """Ensure we can update a notebook correctly when the cell contents change."""
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 0
+
+ cell_1 = workspace.get_text_document(NB_CELL_1.uri)
+ assert cell_1.version == 0
+ assert cell_1.source == "# cell 1"
+
+ cell_2 = workspace.get_text_document(NB_CELL_2.uri)
+ assert cell_2.version == 0
+ assert cell_2.source == "# cell 2"
+
+ params = types.DidChangeNotebookDocumentParams(
+ notebook_document=types.VersionedNotebookDocumentIdentifier(
+ uri=NOTEBOOK.uri, version=31
+ ),
+ change=types.NotebookDocumentChangeEvent(
+ cells=types.NotebookDocumentChangeEventCellsType(
+ text_content=[
+ types.NotebookDocumentChangeEventCellsTypeTextContentType(
+ document=types.VersionedTextDocumentIdentifier(
+ uri=NB_CELL_1.uri, version=13
+ ),
+ changes=[
+ types.TextDocumentContentChangeEvent_Type1(
+ text="new text",
+ range=types.Range(
+ start=types.Position(line=0, character=0),
+ end=types.Position(line=0, character=8),
+ ),
+ )
+ ],
+ ),
+ types.NotebookDocumentChangeEventCellsTypeTextContentType(
+ document=types.VersionedTextDocumentIdentifier(
+ uri=NB_CELL_2.uri, version=21
+ ),
+ changes=[
+ types.TextDocumentContentChangeEvent_Type1(
+ text="",
+ range=types.Range(
+ start=types.Position(line=0, character=0),
+ end=types.Position(line=0, character=8),
+ ),
+ ),
+ types.TextDocumentContentChangeEvent_Type1(
+ text="other text",
+ range=types.Range(
+ start=types.Position(line=0, character=0),
+ end=types.Position(line=0, character=0),
+ ),
+ ),
+ ],
+ ),
+ ]
+ )
+ ),
+ )
+ workspace.update_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 31
+
+ cell_1 = workspace.get_text_document(NB_CELL_1.uri)
+ assert cell_1.version == 13
+ assert cell_1.source == "new text"
+
+ cell_2 = workspace.get_text_document(NB_CELL_2.uri)
+ assert cell_2.version == 21
+ assert cell_2.source == "other text"
+
+
+def test_update_notebook_new_cells(workspace):
+ """Ensure that we can correctly add new cells to an existing notebook."""
+
+ params = types.DidOpenNotebookDocumentParams(
+ notebook_document=NOTEBOOK,
+ cell_text_documents=[
+ NB_CELL_1,
+ NB_CELL_2,
+ ],
+ )
+ workspace.put_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(notebook_uri=NOTEBOOK.uri)
+ assert notebook.version == 0
+
+ cell_uris = [c.document for c in notebook.cells]
+ assert cell_uris == [NB_CELL_1.uri, NB_CELL_2.uri]
+
+ cell_1 = workspace.get_text_document(NB_CELL_1.uri)
+ assert cell_1.version == 0
+ assert cell_1.source == "# cell 1"
+
+ cell_2 = workspace.get_text_document(NB_CELL_2.uri)
+ assert cell_2.version == 0
+ assert cell_2.source == "# cell 2"
+
+ params = types.DidChangeNotebookDocumentParams(
+ notebook_document=types.VersionedNotebookDocumentIdentifier(
+ uri=NOTEBOOK.uri, version=31
+ ),
+ change=types.NotebookDocumentChangeEvent(
+ cells=types.NotebookDocumentChangeEventCellsType(
+ structure=types.NotebookDocumentChangeEventCellsTypeStructureType(
+ array=types.NotebookCellArrayChange(
+ start=1,
+ delete_count=0,
+ cells=[
+ types.NotebookCell(
+ kind=types.NotebookCellKind.Code, document=NB_CELL_3.uri
+ )
+ ],
+ ),
+ did_open=[NB_CELL_3],
+ )
+ )
+ ),
+ )
+ workspace.update_notebook_document(params)
+
+ notebook = workspace.get_notebook_document(cell_uri=NB_CELL_3.uri)
+ assert notebook.uri == NOTEBOOK.uri
+ assert NB_CELL_3.uri in workspace._text_documents
+
+ cell_uris = [c.document for c in notebook.cells]
+ assert cell_uris == [NB_CELL_1.uri, NB_CELL_3.uri, NB_CELL_2.uri]
def test_workspace_folders():
- wf1 = WorkspaceFolder(uri="/ws/f1", name="ws1")
- wf2 = WorkspaceFolder(uri="/ws/f2", name="ws2")
+ wf1 = types.WorkspaceFolder(uri="/ws/f1", name="ws1")
+ wf2 = types.WorkspaceFolder(uri="/ws/f2", name="ws2")
workspace = Workspace("/ws", workspace_folders=[wf1, wf2])