Repository: python-lsp/python-lsp-server
Pull request #470 (2023-10-21 10:48:36)

Add contributing guide to setup dev environment
This was prompted by #216 and I wanted to document how to get started first, because for me at least it was the biggest obstacle (not necessarily because of python-lsp-server itself, but the cooperation with sublime text too). However, I quickly got into troubles as I didn't know what kind of information this guide could and should contain. Not sure if I'll be able to create something good enough without deeper knowledge of pylsp.

staticf0x: @ccordoba12 Could you advise what info would be important to have in a contributing guide?

Patch:
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 96147ba..827c627 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -1,2 +1,5 @@
 # Reformatting with Black
 d47dc3c1fd1f2bafcc079006c3283e465b372f75
+
+# Reformatting with Ruff
+fc2851a23b840481eb92ac522da5bc85305e9d8e
\ No newline at end of file
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index 0e85eb8..d5fd459 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -38,14 +38,12 @@ jobs:
       # If we don't install pycodestyle, pylint will throw an unused-argument error in pylsp/plugins/pycodestyle_lint.py:72
       # This error cannot be resolved by adding a pylint: disable=unused-argument comment ...
       - run: |
-          pip install -e .[pylint,pycodestyle,pyflakes]
-          pip install black
-      - name: Pylint checks
-        run: pylint pylsp test
-      - name: Code style checks with black
-        run: black --check pylsp test
-      - name: Pyflakes checks
-        run: pyflakes pylsp test
+          pip install -e .[pylint,pycodestyle]
+          pip install ruff
+      - name: ruff linter and code style checks
+        run: ruff check pylsp test
+      - name: ruff code formatter check
+        run: ruff format --check pylsp test
       - name: Validate JSON schema
         run: echo {} | jsonschema pylsp/config/schema.json
       - name: Ensure JSON schema and Markdown docs are in sync
diff --git a/.pylintrc b/.pylintrc
index ebe4f30..fe36d8c 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -28,4 +28,4 @@ reports = no
 
 generated-members =
     pylsp_*
-    cache_clear
+    cache_clear
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb9c795..731fe29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,96 @@
 # History of changes
 
+## Version 1.10.1 (2024/03/12)
+
+### Issues Closed
+
+* [Issue 529](https://github.com/python-lsp/python-lsp-server/issues/529) - Autoimports: sqlite3.OperationalError: database is locked ([PR 530](https://github.com/python-lsp/python-lsp-server/pull/530) by [@last-partizan](https://github.com/last-partizan))
+
+In this release 1 issue was closed.
+
+### Pull Requests Merged
+
+* [PR 530](https://github.com/python-lsp/python-lsp-server/pull/530) - Fix progress reporting with autoimport plugin, by [@last-partizan](https://github.com/last-partizan) ([529](https://github.com/python-lsp/python-lsp-server/issues/529))
+* [PR 528](https://github.com/python-lsp/python-lsp-server/pull/528) - Improve error message about missing `websockets` module, by [@tomplus](https://github.com/tomplus)
+
+In this release 2 pull requests were closed.
+
+----
+
+## Version 1.10.0 (2024/01/21)
+
+### New features
+
+* Add support for notebook document completions.
+* Add support for flake8 version 7.
+
+### Issues Closed
+
+* [Issue 513](https://github.com/python-lsp/python-lsp-server/issues/513) - Different versions of autopep can be installed as optional dependencies ([PR 514](https://github.com/python-lsp/python-lsp-server/pull/514) by [@doolio](https://github.com/doolio))
+* [Issue 478](https://github.com/python-lsp/python-lsp-server/issues/478) - Considering pointing to python-lsp-isort rather than pyls-isort in the README ([PR 483](https://github.com/python-lsp/python-lsp-server/pull/483) by [@doolio](https://github.com/doolio))
+* [Issue 474](https://github.com/python-lsp/python-lsp-server/issues/474) - AutoImport can break when being called by multiple threads ([PR 498](https://github.com/python-lsp/python-lsp-server/pull/498) by [@tkrabel](https://github.com/tkrabel))
+* [Issue 373](https://github.com/python-lsp/python-lsp-server/issues/373) - file path auto completion add \ in path string ([PR 497](https://github.com/python-lsp/python-lsp-server/pull/497) by [@i-aki-y](https://github.com/i-aki-y))
+* [Issue 256](https://github.com/python-lsp/python-lsp-server/issues/256) - Flake8 Severity too high ([PR 490](https://github.com/python-lsp/python-lsp-server/pull/490) by [@kunhtkun](https://github.com/kunhtkun))
+
+In this release 5 issues were closed.
+
+### Pull Requests Merged
+
+* [PR 517](https://github.com/python-lsp/python-lsp-server/pull/517) - Combine ruff.toml into pyproject.toml, by [@doolio](https://github.com/doolio)
+* [PR 514](https://github.com/python-lsp/python-lsp-server/pull/514) - Fix optional dependency version for autopep8, by [@doolio](https://github.com/doolio) ([513](https://github.com/python-lsp/python-lsp-server/issues/513))
+* [PR 510](https://github.com/python-lsp/python-lsp-server/pull/510) - Bump flake8 to version 7, by [@bnavigator](https://github.com/bnavigator)
+* [PR 507](https://github.com/python-lsp/python-lsp-server/pull/507) - Fix extra end line increment in autopep8 plugin, by [@remisalmon](https://github.com/remisalmon)
+* [PR 502](https://github.com/python-lsp/python-lsp-server/pull/502) - Use ruff as linter and code formatter, by [@tkrabel](https://github.com/tkrabel)
+* [PR 499](https://github.com/python-lsp/python-lsp-server/pull/499) - Make autoimport cache generation non-blocking, by [@tkrabel](https://github.com/tkrabel)
+* [PR 498](https://github.com/python-lsp/python-lsp-server/pull/498) - Update rope to 1.11.0 for multi-threading capabilities, by [@tkrabel](https://github.com/tkrabel) ([474](https://github.com/python-lsp/python-lsp-server/issues/474))
+* [PR 497](https://github.com/python-lsp/python-lsp-server/pull/497) - Fix path completion when client doesn't support code snippets, by [@i-aki-y](https://github.com/i-aki-y) ([373](https://github.com/python-lsp/python-lsp-server/issues/373))
+* [PR 490](https://github.com/python-lsp/python-lsp-server/pull/490) - Refine diagnostic severity for flake8, by [@kunhtkun](https://github.com/kunhtkun) ([256](https://github.com/python-lsp/python-lsp-server/issues/256))
+* [PR 487](https://github.com/python-lsp/python-lsp-server/pull/487) - Replace call to `python` with `sys.executable` in Pylint plugin, by [@jspricke](https://github.com/jspricke)
+* [PR 486](https://github.com/python-lsp/python-lsp-server/pull/486) - Add support for notebook document completions, by [@smacke](https://github.com/smacke)
+* [PR 483](https://github.com/python-lsp/python-lsp-server/pull/483) - Point to a more up to date isort plugin in README, by [@doolio](https://github.com/doolio) ([478](https://github.com/python-lsp/python-lsp-server/issues/478))
+
+In this release 12 pull requests were closed.
+
+----
+
+## Version 1.9.0 (2023/11/06)
+
+### New features
+
+* Support `initializationOptions` to configure the server.
+* Add code completions to the autoimport plugin.
+* Add support for Pylint 3.
+* Pass `extendIgnore` argument to Flake8.
+* Add new `pylsp_workspace_configuration_changed` hookspec so that plugins can
+  react when client sends a configuration change to the server.
+
+### Issues Closed
+
+* [Issue 460](https://github.com/python-lsp/python-lsp-server/issues/460) - rope_autoimport doesn't initialize after `workspace/didChangeConfiguration` message ([PR 461](https://github.com/python-lsp/python-lsp-server/pull/461) by [@tkrabel-db](https://github.com/tkrabel-db))
+* [Issue 403](https://github.com/python-lsp/python-lsp-server/issues/403) - Add code action for implementing auto-import ([PR 471](https://github.com/python-lsp/python-lsp-server/pull/471) by [@tkrabel-db](https://github.com/tkrabel-db))
+* [Issue 195](https://github.com/python-lsp/python-lsp-server/issues/195) - Maybe use initializationOptions as additional source of settings ([PR 459](https://github.com/python-lsp/python-lsp-server/pull/459) by [@tkrabel-db](https://github.com/tkrabel-db))
+
+In this release 3 issues were closed.
+
+### Pull Requests Merged
+
+* [PR 481](https://github.com/python-lsp/python-lsp-server/pull/481) - Revert "Rename `_utils` module to `utils`", by [@ccordoba12](https://github.com/ccordoba12)
+* [PR 480](https://github.com/python-lsp/python-lsp-server/pull/480) - Rename `_utils` module to `utils`, by [@ccordoba12](https://github.com/ccordoba12)
+* [PR 475](https://github.com/python-lsp/python-lsp-server/pull/475) - Raise supported Pylint upper version, by [@bnavigator](https://github.com/bnavigator)
+* [PR 473](https://github.com/python-lsp/python-lsp-server/pull/473) - Improve/simplify README Development section, by [@tkrabel](https://github.com/tkrabel)
+* [PR 471](https://github.com/python-lsp/python-lsp-server/pull/471) - Add code completions to `rope_autoimport` plugin, by [@tkrabel-db](https://github.com/tkrabel-db) ([403](https://github.com/python-lsp/python-lsp-server/issues/403))
+* [PR 469](https://github.com/python-lsp/python-lsp-server/pull/469) - Pass argument `extendIgnore` to flake8, by [@UnkwUsr](https://github.com/UnkwUsr)
+* [PR 466](https://github.com/python-lsp/python-lsp-server/pull/466) - Ignore notebook names on cell completion for autoimport, by [@tkrabel-db](https://github.com/tkrabel-db)
+* [PR 464](https://github.com/python-lsp/python-lsp-server/pull/464) - Minor bug fix in Rope autoimport plugin, by [@tkrabel-db](https://github.com/tkrabel-db)
+* [PR 462](https://github.com/python-lsp/python-lsp-server/pull/462) - Make workspace/didChangeConfig work with notebook documents, by [@tkrabel-db](https://github.com/tkrabel-db)
+* [PR 461](https://github.com/python-lsp/python-lsp-server/pull/461) - Load `rope_autoimport` cache on `workspace/didChangeConfiguration`, by [@tkrabel-db](https://github.com/tkrabel-db) ([460](https://github.com/python-lsp/python-lsp-server/issues/460))
+* [PR 459](https://github.com/python-lsp/python-lsp-server/pull/459) - Support `initializationOptions` to configure the server, by [@tkrabel-db](https://github.com/tkrabel-db) ([195](https://github.com/python-lsp/python-lsp-server/issues/195))
+* [PR 457](https://github.com/python-lsp/python-lsp-server/pull/457) - Fix missing signatures for docstrings in Markdown, by [@staticf0x](https://github.com/staticf0x)
+
+In this release 12 pull requests were closed.
+
+----
+
 ## Version 1.8.2 (2023/10/09)
 
 ### Issues Closed
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
index f88e425..acf8a85 100644
--- a/CONFIGURATION.md
+++ b/CONFIGURATION.md
@@ -64,7 +64,9 @@ This server can be configured using the `workspace/didChangeConfiguration` metho
 | `pylsp.plugins.pylint.enabled` | `boolean` | Enable or disable the plugin. | `false` |
 | `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `[]` |
 | `pylsp.plugins.pylint.executable` | `string` | Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3. | `null` |
-| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable autoimport. | `false` |
+| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable autoimport. If false, neither completions nor code actions are enabled. If true, the respective features can be enabled or disabled individually. | `false` |
+| `pylsp.plugins.rope_autoimport.completions.enabled` | `boolean` | Enable or disable autoimport completions. | `true` |
+| `pylsp.plugins.rope_autoimport.code_actions.enabled` | `boolean` | Enable or disable autoimport code actions (e.g. for quick fixes). | `true` |
 | `pylsp.plugins.rope_autoimport.memory` | `boolean` | Make the autoimport database memory only. Drastically increases startup time. | `false` |
 | `pylsp.plugins.rope_completion.enabled` | `boolean` | Enable or disable the plugin. | `false` |
 | `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..5a24957
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,40 @@
+# Setup the environment
+
+1. Clone the repo: `git clone git@github.com:python-lsp/python-lsp-server.git`
+2. Create the virtual environment: `python -m venv .venv`
+3. Activate: `source .venv/bin/activate`
+4. Install an editable installation: `pip install -e .`
+    - This will ensure you'll see your edits immediately without reinstalling the project
+5. Configure your editor to point the pylsp executable to the one in `.venv`
+
+## Trying out if it works
+
+Go to file `pylsp/python_lsp.py`, function `start_io_lang_server`,
+and on the first line of the function, add some logging:
+
+```py
+log.info("It works!")
+```
+
+Save the file, restart the LSP server and you should see the log line:
+
+```
+2023-10-12 16:46:38,320 CEST - INFO - pylsp._utils - It works!
+```
+
+Now the project is setup in a way you can quickly iterate change you want to add.
+
+# Running tests
+
+1. Install runtime dependencies: `pip install .[all]`
+2. Install test dependencies: `pip install .[test]`
+3. Run `pytest`: `pytest -v`
+
+## Useful pytest options
+
+- To run a specific test file, use `pytest test/test_utils.py`
+- To run a specific test function within a test file,
+  use `pytest test/test_utils.py::test_debounce`
+- To run tests matching a certain expression, use `pytest -k format`
+- To increase verbosity of pytest, use `pytest -v` or `pytest -vv`
+- To enter a debugger on failed tests, use `pytest --pdb`
diff --git a/MANIFEST.in b/MANIFEST.in
index e82d1f5..52e7714 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,5 +2,6 @@ include README.md
 include versioneer.py
 include pylsp/_version.py
 include LICENSE
+include ruff.toml
 include .pylintrc
 recursive-include test *.py
diff --git a/README.md b/README.md
index 2e2c721..85d6452 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ pip install -U setuptools
 If you use Anaconda/Miniconda, you can install `python-lsp-server` using this conda command
 
 ```
-conda install -c conda-forge python-lsp-server 
+conda install -c conda-forge python-lsp-server
 ```
 
 Python-lsp-server is available in the repos of every major Linux distribution, and it is usually called `python-lsp-server` or `python3-pylsp`.
@@ -82,7 +82,7 @@ apk add py3-lsp-server
 Installing these plugins will add extra functionality to the language server:
 
 - [pylsp-mypy](https://github.com/Richardk2n/pylsp-mypy): [MyPy](http://mypy-lang.org/) type checking for Python >=3.8.
-- [pyls-isort](https://github.com/paradoxxxzero/pyls-isort): code formatting using [isort](https://github.com/PyCQA/isort) (automatic import sorting).
+- [python-lsp-isort](https://github.com/chantera/python-lsp-isort): code formatting using [isort](https://github.com/PyCQA/isort) (automatic import sorting).
 - [python-lsp-black](https://github.com/python-lsp/python-lsp-black): code formatting using [Black](https://github.com/psf/black).
 - [pyls-memestra](https://github.com/QuantStack/pyls-memestra): detecting the use of deprecated APIs.
 - [pylsp-rope](https://github.com/python-rope/pylsp-rope): Extended refactoring capabilities using [Rope](https://github.com/python-rope/rope).
@@ -101,7 +101,7 @@ Like all language servers, configuration can be passed from the client that talk
 `python-lsp-server` depends on other tools, like flake8 and pycodestyle. These tools can be configured via settings passed from the client (as above), or alternatively from other configuration sources. The following sources are available:
 
 - `pycodestyle`: discovered in `~/.config/pycodestyle`, `setup.cfg`, `tox.ini` and `pycodestyle.cfg`.
-- `flake8`: discovered in `~/.config/flake8`, `.flake8`, `setup.cfg` and `tox.ini`
+- `flake8`: discovered in `.flake8`, `setup.cfg` and `tox.ini`
 
 The default configuration sources are `pycodestyle` and `pyflakes`. If you would like to use `flake8`, you will need to:
 
@@ -164,25 +164,32 @@ pip install 'python-lsp-server[websockets]'
 Dev install
 
 ```
-# create conda env
-conda create --name python-lsp-server python=3.8 -y
+# (optional) create conda env
+conda create --name python-lsp-server python=3.11 -y
 conda activate python-lsp-server
 
-pip install ".[all]"
-pip install ".[websockets]"
+pip install -e ".[all,websockets,test]"
 ```
 
 Run server with ws
 
 ```
-pylsp --ws -v    # Info level logging
-pylsp --ws -v -v # Debug level logging
+pylsp --ws -v  # Info level logging
+pylsp --ws -vv # Debug level logging
 ```
 
 To run the test suite:
 
 ```sh
-pip install ".[test]" && pytest
+# requires: pip install ".[test]" (see above)
+pytest
+```
+
+Running ruff as a linter and code formatter on the repo:
+```sh
+ruff check .  # linter
+ruff check --fix .  # fix all auto-fixable lint issues
+ruff format .  # format the document
 ```
 
 After adding configuration options to `schema.json`, refresh the `CONFIGURATION.md` file with
diff --git a/docs/autoimport.md b/docs/autoimport.md
index 5bf573e..893a5e9 100644
--- a/docs/autoimport.md
+++ b/docs/autoimport.md
@@ -4,6 +4,7 @@ Requirements:
 
 1. install `python-lsp-server[rope]`
 2. set `pylsp.plugins.rope_autoimport.enabled` to `true`
+3. This enables both completions and code actions. You can switch them off by setting `pylsp.plugins.rope_autoimport.completions.enabled` and/or `pylsp.plugins.rope_autoimport.code_actions.enabled` to `false`
 
 ## Startup
 
diff --git a/pylsp/__init__.py b/pylsp/__init__.py
index eeb80bd..f363ad8 100644
--- a/pylsp/__init__.py
+++ b/pylsp/__init__.py
@@ -2,7 +2,9 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import os
+
 import pluggy
+
 from . import _version
 from ._version import __version__
 
diff --git a/pylsp/__main__.py b/pylsp/__main__.py
index d669174..61bd015 100644
--- a/pylsp/__main__.py
+++ b/pylsp/__main__.py
@@ -9,16 +9,16 @@ import time
 
 try:
     import ujson as json
-except Exception:  # pylint: disable=broad-except
+except Exception:
     import json
 
+from ._version import __version__
 from .python_lsp import (
     PythonLSPServer,
     start_io_lang_server,
     start_tcp_lang_server,
     start_ws_lang_server,
 )
-from ._version import __version__
 
 LOG_FORMAT = "%(asctime)s {0} - %(levelname)s - %(name)s - %(message)s".format(
     time.localtime().tm_zone
diff --git a/pylsp/_utils.py b/pylsp/_utils.py
index 9d393b9..0293ee3 100644
--- a/pylsp/_utils.py
+++ b/pylsp/_utils.py
@@ -8,6 +8,7 @@ import os
 import pathlib
 import re
 import threading
+import time
 from typing import List, Optional
 
 import docstring_to_markdown
@@ -55,6 +56,23 @@ def debounce(interval_s, keyed_by=None):
     return wrapper
 
 
+def throttle(seconds=1):
+    """Throttles calls to a function evey `seconds` seconds."""
+
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(*args, **kwargs):
+            if not hasattr(wrapper, "last_call"):
+                wrapper.last_call = 0
+            if time.time() - wrapper.last_call >= seconds:
+                wrapper.last_call = time.time()
+                return func(*args, **kwargs)
+
+        return wrapper
+
+    return decorator
+
+
 def find_parents(root, path, names):
     """Find files matching the given names relative to the given path.
 
@@ -157,8 +175,8 @@ def escape_plain_text(contents: str) -> str:
     """
     Format plain text to display nicely in environments which do not respect whitespaces.
     """
-    contents = contents.replace("\t", "\u00A0" * 4)
-    contents = contents.replace("  ", "\u00A0" * 2)
+    contents = contents.replace("\t", "\u00a0" * 4)
+    contents = contents.replace("  ", "\u00a0" * 2)
     return contents
 
 
diff --git a/pylsp/config/config.py b/pylsp/config/config.py
index 454ee4b..0d9f263 100644
--- a/pylsp/config/config.py
+++ b/pylsp/config/config.py
@@ -1,6 +1,5 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
-# pylint: disable=import-outside-toplevel
 
 import logging
 import sys
@@ -10,7 +9,7 @@ from typing import List, Mapping, Sequence, Union
 import pluggy
 from pluggy._hooks import HookImpl
 
-from pylsp import _utils, hookspecs, uris, PYLSP
+from pylsp import PYLSP, _utils, hookspecs, uris
 
 # See compatibility note on `group` keyword:
 #   https://docs.python.org/3/library/importlib.metadata.html#entry-points
@@ -38,7 +37,7 @@ class PluginManager(pluggy.PluginManager):
         # enable_tracing will set its own wrapping function at self._inner_hookexec
         try:
             return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
-        except Exception as e:  # pylint: disable=broad-except
+        except Exception as e:
             log.warning(f"Failed to load hook {hook_name}: {e}", exc_info=True)
             return []
 
@@ -79,7 +78,7 @@ class Config:
         for entry_point in entry_points(group=PYLSP):
             try:
                 entry_point.load()
-            except Exception as e:  # pylint: disable=broad-except
+            except Exception as e:
                 log.info(
                     "Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e
                 )
diff --git a/pylsp/config/flake8_conf.py b/pylsp/config/flake8_conf.py
index ca3b199..5e969d9 100644
--- a/pylsp/config/flake8_conf.py
+++ b/pylsp/config/flake8_conf.py
@@ -3,7 +3,9 @@
 
 import logging
 import os
+
 from pylsp._utils import find_parents
+
 from .source import ConfigSource
 
 log = logging.getLogger(__name__)
diff --git a/pylsp/config/pycodestyle_conf.py b/pylsp/config/pycodestyle_conf.py
index 98d8a1b..ed15a80 100644
--- a/pylsp/config/pycodestyle_conf.py
+++ b/pylsp/config/pycodestyle_conf.py
@@ -2,9 +2,10 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import pycodestyle
+
 from pylsp._utils import find_parents
-from .source import ConfigSource
 
+from .source import ConfigSource
 
 CONFIG_KEY = "pycodestyle"
 USER_CONFIGS = [pycodestyle.USER_CONFIG] if pycodestyle.USER_CONFIG else []
diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json
index fbf7f01..ba1d36f 100644
--- a/pylsp/config/schema.json
+++ b/pylsp/config/schema.json
@@ -6,11 +6,16 @@
   "properties": {
     "pylsp.configurationSources": {
       "type": "array",
-      "default": ["pycodestyle"],
+      "default": [
+        "pycodestyle"
+      ],
       "description": "List of configuration sources to use.",
       "items": {
         "type": "string",
-        "enum": ["pycodestyle", "flake8"]
+        "enum": [
+          "pycodestyle",
+          "flake8"
+        ]
       },
       "uniqueItems": true
     },
@@ -20,7 +25,10 @@
       "description": "Enable or disable the plugin (disabling required to use `yapf`)."
     },
     "pylsp.plugins.flake8.config": {
-      "type": ["string", "null"],
+      "type": [
+        "string",
+        "null"
+      ],
       "default": null,
       "description": "Path to the config file that will be the authoritative config source."
     },
@@ -51,12 +59,18 @@
       "description": "Path to the flake8 executable."
     },
     "pylsp.plugins.flake8.filename": {
-      "type": ["string", "null"],
+      "type": [
+        "string",
+        "null"
+      ],
       "default": null,
       "description": "Only check for filenames matching the patterns in this list."
     },
     "pylsp.plugins.flake8.hangClosing": {
-      "type": ["boolean", "null"],
+      "type": [
+        "boolean",
+        "null"
+      ],
       "default": null,
       "description": "Hang closing bracket instead of matching indentation of opening bracket's line."
     },
@@ -74,17 +88,25 @@
       "description": "Maximum allowed complexity threshold."
     },
     "pylsp.plugins.flake8.maxLineLength": {
-      "type": ["integer", "null"],
+      "type": [
+        "integer",
+        "null"
+      ],
       "default": null,
       "description": "Maximum allowed line length for the entirety of this run."
     },
     "pylsp.plugins.flake8.indentSize": {
-      "type": ["integer", "null"],
+      "type": [
+        "integer",
+        "null"
+      ],
       "default": null,
       "description": "Set indentation spaces."
     },
     "pylsp.plugins.flake8.perFileIgnores": {
-      "type": ["array"],
+      "type": [
+        "array"
+      ],
       "default": [],
       "items": {
         "type": "string"
@@ -92,7 +114,10 @@
       "description": "A pairing of filenames and violation codes that defines which violations to ignore in a particular file, for example: `[\"file_path.py:W305,W304\"]`)."
     },
     "pylsp.plugins.flake8.select": {
-      "type": ["array", "null"],
+      "type": [
+        "array",
+        "null"
+      ],
       "default": null,
       "items": {
         "type": "string"
@@ -102,7 +127,9 @@
     },
     "pylsp.plugins.jedi.auto_import_modules": {
       "type": "array",
-      "default": ["numpy"],
+      "default": [
+        "numpy"
+      ],
       "items": {
         "type": "string"
       },
@@ -117,12 +144,18 @@
       "description": "Define extra paths for jedi.Script."
     },
     "pylsp.plugins.jedi.env_vars": {
-      "type": ["object", "null"],
+      "type": [
+        "object",
+        "null"
+      ],
       "default": null,
       "description": "Define environment variables for jedi.Script and Jedi.names."
     },
     "pylsp.plugins.jedi.environment": {
-      "type": ["string", "null"],
+      "type": [
+        "string",
+        "null"
+      ],
       "default": null,
       "description": "Define environment for jedi.Script and Jedi.names."
     },
@@ -166,7 +199,12 @@
       "items": {
         "type": "string"
       },
-      "default": ["pandas", "numpy", "tensorflow", "matplotlib"],
+      "default": [
+        "pandas",
+        "numpy",
+        "tensorflow",
+        "matplotlib"
+      ],
       "description": "Modules for which labels and snippets should be cached."
     },
     "pylsp.plugins.jedi_definition.enabled": {
@@ -267,7 +305,10 @@
       "description": "When parsing directories, only check filenames matching these patterns."
     },
     "pylsp.plugins.pycodestyle.select": {
-      "type": ["array", "null"],
+      "type": [
+        "array",
+        "null"
+      ],
       "default": null,
       "items": {
         "type": "string"
@@ -285,17 +326,26 @@
       "description": "Ignore errors and warnings"
     },
     "pylsp.plugins.pycodestyle.hangClosing": {
-      "type": ["boolean", "null"],
+      "type": [
+        "boolean",
+        "null"
+      ],
       "default": null,
       "description": "Hang closing bracket instead of matching indentation of opening bracket's line."
     },
     "pylsp.plugins.pycodestyle.maxLineLength": {
-      "type": ["integer", "null"],
+      "type": [
+        "integer",
+        "null"
+      ],
       "default": null,
       "description": "Set maximum allowed line length."
     },
     "pylsp.plugins.pycodestyle.indentSize": {
-      "type": ["integer", "null"],
+      "type": [
+        "integer",
+        "null"
+      ],
       "default": null,
       "description": "Set indentation spaces."
     },
@@ -305,9 +355,17 @@
       "description": "Enable or disable the plugin."
     },
     "pylsp.plugins.pydocstyle.convention": {
-      "type": ["string", "null"],
+      "type": [
+        "string",
+        "null"
+      ],
       "default": null,
-      "enum": ["pep257", "numpy", "google", null],
+      "enum": [
+        "pep257",
+        "numpy",
+        "google",
+        null
+      ],
       "description": "Choose the basic list of checked errors by specifying an existing convention."
     },
     "pylsp.plugins.pydocstyle.addIgnore": {
@@ -338,7 +396,10 @@
       "description": "Ignore errors and warnings"
     },
     "pylsp.plugins.pydocstyle.select": {
-      "type": ["array", "null"],
+      "type": [
+        "array",
+        "null"
+      ],
       "default": null,
       "items": {
         "type": "string"
@@ -376,14 +437,27 @@
       "description": "Arguments to pass to pylint."
     },
     "pylsp.plugins.pylint.executable": {
-      "type": ["string", "null"],
+      "type": [
+        "string",
+        "null"
+      ],
       "default": null,
       "description": "Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3."
     },
     "pylsp.plugins.rope_autoimport.enabled": {
       "type": "boolean",
       "default": false,
-      "description": "Enable or disable autoimport."
+      "description": "Enable or disable autoimport. If false, neither completions nor code actions are enabled. If true, the respective features can be enabled or disabled individually."
+    },
+    "pylsp.plugins.rope_autoimport.completions.enabled": {
+      "type": "boolean",
+      "default": true,
+      "description": "Enable or disable autoimport completions."
+    },
+    "pylsp.plugins.rope_autoimport.code_actions.enabled": {
+      "type": "boolean",
+      "default": true,
+      "description": "Enable or disable autoimport code actions (e.g. for quick fixes)."
     },
     "pylsp.plugins.rope_autoimport.memory": {
       "type": "boolean",
@@ -406,12 +480,18 @@
       "description": "Enable or disable the plugin."
     },
     "pylsp.rope.extensionModules": {
-      "type": ["string", "null"],
+      "type": [
+        "string",
+        "null"
+      ],
       "default": null,
       "description": "Builtin and c-extension modules that are allowed to be imported and inspected by rope."
     },
     "pylsp.rope.ropeFolder": {
-      "type": ["array", "null"],
+      "type": [
+        "array",
+        "null"
+      ],
       "default": null,
       "items": {
         "type": "string"
@@ -420,4 +500,4 @@
       "description": "The name of the folder in which rope stores project configurations and data.  Pass `null` for not using such a folder at all."
     }
   }
-}
+}
\ No newline at end of file
diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py
index 9c9cf38..a2549fb 100644
--- a/pylsp/hookspecs.py
+++ b/pylsp/hookspecs.py
@@ -1,7 +1,6 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-# pylint: disable=redefined-builtin, unused-argument
 from pylsp import hookspec
 
 
diff --git a/pylsp/plugins/_resolvers.py b/pylsp/plugins/_resolvers.py
index d0d2dc4..835b3af 100644
--- a/pylsp/plugins/_resolvers.py
+++ b/pylsp/plugins/_resolvers.py
@@ -1,15 +1,14 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-from collections import defaultdict
 import logging
+from collections import defaultdict
 from time import time
 
 from jedi.api.classes import Completion
 
 from pylsp import lsp
 
-
 log = logging.getLogger(__name__)
 
 
@@ -77,7 +76,7 @@ class Resolver:
         try:
             sig = completion.get_signatures()
             return self.callback(completion, sig)
-        except Exception as e:  # pylint: disable=broad-except
+        except Exception as e:
             log.warning(
                 f"Something went wrong when resolving label for {completion}: {e}"
             )
diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py
index ed93a05..a72ef56 100644
--- a/pylsp/plugins/_rope_task_handle.py
+++ b/pylsp/plugins/_rope_task_handle.py
@@ -5,6 +5,7 @@ from typing import Callable, ContextManager, List, Optional, Sequence
 
 from rope.base.taskhandle import BaseJobSet, BaseTaskHandle
 
+from pylsp._utils import throttle
 from pylsp.workspace import Workspace
 
 log = logging.getLogger(__name__)
@@ -55,6 +56,7 @@ class PylspJobSet(BaseJobSet):
         self.count += 1
         self._report()
 
+    @throttle(0.5)
     def _report(self):
         percent = int(self.get_percent_done())
         message = f"{self.job_name} {self.done}/{self.count}"
diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py
index 1ae3e5f..2b3491d 100644
--- a/pylsp/plugins/autopep8_format.py
+++ b/pylsp/plugins/autopep8_format.py
@@ -4,7 +4,8 @@
 import logging
 
 import pycodestyle
-from autopep8 import fix_code, continued_indentation as autopep8_c_i
+from autopep8 import continued_indentation as autopep8_c_i
+from autopep8 import fix_code
 
 from pylsp import hookimpl
 from pylsp._utils import get_eol_chars
@@ -13,18 +14,14 @@ log = logging.getLogger(__name__)
 
 
 @hookimpl(tryfirst=True)  # Prefer autopep8 over YAPF
-def pylsp_format_document(
-    config, workspace, document, options
-):  # pylint: disable=unused-argument
+def pylsp_format_document(config, workspace, document, options):
     with workspace.report_progress("format: autopep8"):
         log.info("Formatting document %s with autopep8", document)
         return _format(config, document)
 
 
 @hookimpl(tryfirst=True)  # Prefer autopep8 over YAPF
-def pylsp_format_range(
-    config, workspace, document, range, options
-):  # pylint: disable=redefined-builtin,unused-argument
+def pylsp_format_range(config, workspace, document, range, options):
     log.info("Formatting document %s in range %s with autopep8", document, range)
 
     # First we 'round' the range up/down to full lines only
@@ -33,7 +30,7 @@ def pylsp_format_range(
     range["end"]["character"] = 0
 
     # Add 1 for 1-indexing vs LSP's 0-indexing
-    line_range = (range["start"]["line"] + 1, range["end"]["line"] + 1)
+    line_range = (range["start"]["line"] + 1, range["end"]["line"])
     return _format(config, document, line_range=line_range)
 
 
diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py
index 53eda91..67abfb7 100644
--- a/pylsp/plugins/definition.py
+++ b/pylsp/plugins/definition.py
@@ -1,16 +1,18 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 from __future__ import annotations
+
 import logging
-from typing import Any, Dict, List, TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Dict, List
 
 import jedi
 
-from pylsp import hookimpl, uris, _utils
+from pylsp import _utils, hookimpl, uris
 
 if TYPE_CHECKING:
     from jedi.api import Script
     from jedi.api.classes import Name
+
     from pylsp.config.config import Config
     from pylsp.workspace import Document
 
diff --git a/pylsp/plugins/flake8_lint.py b/pylsp/plugins/flake8_lint.py
index 8d8d4c5..4712101 100644
--- a/pylsp/plugins/flake8_lint.py
+++ b/pylsp/plugins/flake8_lint.py
@@ -2,6 +2,7 @@
 # Copyright 2021- Python Language Server Contributors.
 
 """Linter pluging for flake8"""
+
 import logging
 import os.path
 import re
@@ -9,9 +10,13 @@ import sys
 from pathlib import PurePath
 from subprocess import PIPE, Popen
 
+from flake8.plugins.pyflakes import FLAKE8_PYFLAKES_CODES
+
 from pylsp import hookimpl, lsp
+from pylsp.plugins.pyflakes_lint import PYFLAKES_ERROR_MESSAGES
 
 log = logging.getLogger(__name__)
+
 FIX_IGNORES_RE = re.compile(r"([^a-zA-Z0-9_,]*;.*(\W+||$))")
 UNNECESSITY_CODES = {
     "F401",  # `module` imported but unused
@@ -20,6 +25,14 @@ UNNECESSITY_CODES = {
     "F523",  # .format(...) unused positional arguments
     "F841",  # local variable `name` is assigned to but never used
 }
+# NOTE: If the user sets the flake8 executable with workspace configuration, the
+# error codes in this set may be inaccurate.
+ERROR_CODES = (
+    # Errors from the pyflakes plugin of flake8
+    {FLAKE8_PYFLAKES_CODES.get(m.__name__, "E999") for m in PYFLAKES_ERROR_MESSAGES}
+    # Syntax error from flake8 itself
+    | {"E999"}
+)
 
 
 @hookimpl
@@ -129,9 +142,7 @@ def run_flake8(flake8_executable, args, document, source):
         )
         cmd = [sys.executable, "-m", "flake8"]
         cmd.extend(args)
-        p = Popen(  # pylint: disable=consider-using-with
-            cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs
-        )
+        p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs)
     (stdout, stderr) = p.communicate(source.encode())
     if stderr:
         log.error("Error while running flake8 '%s'", stderr.decode())
@@ -208,7 +219,7 @@ def parse_stdout(source, stdout):
         # show also the code in message
         msg = code + " " + msg
         severity = lsp.DiagnosticSeverity.Warning
-        if code == "E999" or code[0] == "F":
+        if code in ERROR_CODES:
             severity = lsp.DiagnosticSeverity.Error
         diagnostic = {
             "source": "flake8",
diff --git a/pylsp/plugins/highlight.py b/pylsp/plugins/highlight.py
index 0dd896c..c4c1240 100644
--- a/pylsp/plugins/highlight.py
+++ b/pylsp/plugins/highlight.py
@@ -2,7 +2,8 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
-from pylsp import hookimpl, lsp, _utils
+
+from pylsp import _utils, hookimpl, lsp
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/plugins/hover.py b/pylsp/plugins/hover.py
index ae07b3d..ca69d1b 100644
--- a/pylsp/plugins/hover.py
+++ b/pylsp/plugins/hover.py
@@ -3,7 +3,7 @@
 
 import logging
 
-from pylsp import hookimpl, _utils
+from pylsp import _utils, hookimpl
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py
index 2ecf0be..2796a09 100644
--- a/pylsp/plugins/jedi_completion.py
+++ b/pylsp/plugins/jedi_completion.py
@@ -38,7 +38,6 @@ _ERRORS = ("error_node",)
 @hookimpl
 def pylsp_completions(config, document, position):
     """Get formatted completions for current code position"""
-    # pylint: disable=too-many-locals
     settings = config.plugin_settings("jedi_completion", document_path=document.path)
     resolve_eagerly = settings.get("eager", False)
     code_position = _utils.position_to_jedi_linecolumn(document, position)
@@ -88,6 +87,7 @@ def pylsp_completions(config, document, position):
             include_params=include_params if c.type in ["class", "function"] else False,
             resolve=resolve_eagerly,
             resolve_label_or_snippet=(i < max_to_resolve),
+            snippet_support=snippet_support,
         )
         for i, c in enumerate(completions)
     ]
@@ -102,6 +102,7 @@ def pylsp_completions(config, document, position):
                     include_params=False,
                     resolve=resolve_eagerly,
                     resolve_label_or_snippet=(i < max_to_resolve),
+                    snippet_support=snippet_support,
                 )
                 completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
                 completion_dict["label"] += " object"
@@ -116,6 +117,7 @@ def pylsp_completions(config, document, position):
                     include_params=False,
                     resolve=resolve_eagerly,
                     resolve_label_or_snippet=(i < max_to_resolve),
+                    snippet_support=snippet_support,
                 )
                 completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
                 completion_dict["label"] += " object"
@@ -206,7 +208,6 @@ def use_snippets(document, position):
 
 
 def _resolve_completion(completion, d, markup_kind: str):
-    # pylint: disable=broad-except
     completion["detail"] = _detail(d)
     try:
         docs = _utils.format_docstring(
@@ -226,6 +227,7 @@ def _format_completion(
     include_params=True,
     resolve=False,
     resolve_label_or_snippet=False,
+    snippet_support=False,
 ):
     completion = {
         "label": _label(d, resolve_label_or_snippet),
@@ -240,16 +242,20 @@ def _format_completion(
     # Adjustments for file completions
     if d.type == "path":
         path = os.path.normpath(d.name)
-        path = path.replace("\\", "\\\\")
-        path = path.replace("/", "\\/")
 
-        # If the completion ends with os.sep, it means it's a directory. So we add an escaped os.sep
-        # at the end to ease additional file completions.
+        # If the completion ends with os.sep, it means it's a directory. So we add os.sep at the end
+        # to ease additional file completions.
         if d.name.endswith(os.sep):
             if os.name == "nt":
-                path = path + "\\\\"
+                path = path + "\\"
             else:
-                path = path + "\\/"
+                path = path + "/"
+
+        # Escape to prevent conflicts with the code snippets grammer
+        # See also https://github.com/python-lsp/python-lsp-server/issues/373
+        if snippet_support:
+            path = path.replace("\\", "\\\\")
+            path = path.replace("/", "\\/")
 
         completion["insertText"] = path
 
diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py
index 9c89c1d..5606a88 100644
--- a/pylsp/plugins/jedi_rename.py
+++ b/pylsp/plugins/jedi_rename.py
@@ -3,15 +3,13 @@
 
 import logging
 
-from pylsp import hookimpl, uris, _utils
+from pylsp import _utils, hookimpl, uris
 
 log = logging.getLogger(__name__)
 
 
 @hookimpl
-def pylsp_rename(
-    config, workspace, document, position, new_name
-):  # pylint: disable=unused-argument
+def pylsp_rename(config, workspace, document, position, new_name):
     log.debug(
         "Executing rename of %s to %s", document.word_at_position(position), new_name
     )
@@ -20,7 +18,6 @@ def pylsp_rename(
     try:
         refactoring = document.jedi_script().rename(**kwargs)
     except NotImplementedError as exc:
-        # pylint: disable=broad-exception-raised
         raise Exception(
             "No support for renaming in Python 2/3.5 with Jedi. "
             "Consider using the rope_rename plugin instead"
diff --git a/pylsp/plugins/mccabe_lint.py b/pylsp/plugins/mccabe_lint.py
index f115a3c..0e2cba2 100644
--- a/pylsp/plugins/mccabe_lint.py
+++ b/pylsp/plugins/mccabe_lint.py
@@ -3,7 +3,9 @@
 
 import ast
 import logging
+
 import mccabe
+
 from pylsp import hookimpl, lsp
 
 log = logging.getLogger(__name__)
diff --git a/pylsp/plugins/preload_imports.py b/pylsp/plugins/preload_imports.py
index 0b98feb..a45a666 100644
--- a/pylsp/plugins/preload_imports.py
+++ b/pylsp/plugins/preload_imports.py
@@ -2,6 +2,7 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
+
 from pylsp import hookimpl
 
 log = logging.getLogger(__name__)
@@ -71,7 +72,7 @@ def pylsp_initialize(config):
         try:
             __import__(mod_name)
             log.debug("Preloaded module %s", mod_name)
-        except Exception:  # pylint: disable=broad-except
+        except Exception:
             # Catch any exception since not only ImportError can be raised here
             # For example, old versions of NumPy can cause a ValueError.
             # See spyder-ide/spyder#13985
diff --git a/pylsp/plugins/pydocstyle_lint.py b/pylsp/plugins/pydocstyle_lint.py
index 3a4df1c..7d04bdf 100644
--- a/pylsp/plugins/pydocstyle_lint.py
+++ b/pylsp/plugins/pydocstyle_lint.py
@@ -8,6 +8,7 @@ import re
 import sys
 
 import pydocstyle
+
 from pylsp import hookimpl, lsp
 
 log = logging.getLogger(__name__)
@@ -28,7 +29,6 @@ def pylsp_settings():
 
 @hookimpl
 def pylsp_lint(config, workspace, document):
-    # pylint: disable=too-many-locals
     with workspace.report_progress("lint: pydocstyle"):
         settings = config.plugin_settings("pydocstyle", document_path=document.path)
         log.debug("Got pydocstyle settings: %s", settings)
diff --git a/pylsp/plugins/pyflakes_lint.py b/pylsp/plugins/pyflakes_lint.py
index 28a37b3..e23f9a0 100644
--- a/pylsp/plugins/pyflakes_lint.py
+++ b/pylsp/plugins/pyflakes_lint.py
@@ -1,7 +1,9 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-from pyflakes import api as pyflakes_api, messages
+from pyflakes import api as pyflakes_api
+from pyflakes import messages
+
 from pylsp import hookimpl, lsp
 
 # Pyflakes messages that should be reported as Errors instead of Warns
diff --git a/pylsp/plugins/pylint_lint.py b/pylsp/plugins/pylint_lint.py
index 67f68fa..beffe6f 100644
--- a/pylsp/plugins/pylint_lint.py
+++ b/pylsp/plugins/pylint_lint.py
@@ -3,19 +3,20 @@
 # Copyright 2021- Python Language Server Contributors.
 
 """Linter plugin for pylint."""
+
 import collections
 import logging
-import sys
-import re
-from subprocess import Popen, PIPE
 import os
+import re
 import shlex
+import sys
+from subprocess import PIPE, Popen
 
 from pylsp import hookimpl, lsp
 
 try:
     import ujson as json
-except Exception:  # pylint: disable=broad-except
+except Exception:
     import json
 
 log = logging.getLogger(__name__)
@@ -48,9 +49,7 @@ class PylintLinter:
     last_diags = collections.defaultdict(list)
 
     @classmethod
-    def lint(
-        cls, document, is_saved, flags=""
-    ):  # pylint: disable=too-many-locals,too-many-branches
+    def lint(cls, document, is_saved, flags=""):
         """Plugin interface to pylsp linter.
 
         Args:
@@ -286,12 +285,10 @@ def _run_pylint_stdio(pylint_executable, document, flags):
         p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
     except IOError:
         log.debug("Can't execute %s. Trying with 'python -m pylint'", pylint_executable)
-        cmd = ["python", "-m", "pylint"]
+        cmd = [sys.executable, "-m", "pylint"]
         cmd.extend(flags)
         cmd.extend(["--from-stdin", document.path])
-        p = Popen(  # pylint: disable=consider-using-with
-            cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE
-        )
+        p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
     (stdout, stderr) = p.communicate(document.source.encode())
     if stderr:
         log.error("Error while running pylint '%s'", stderr.decode())
diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py
index fadf1de..a4c61b5 100644
--- a/pylsp/plugins/references.py
+++ b/pylsp/plugins/references.py
@@ -2,7 +2,8 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
-from pylsp import hookimpl, uris, _utils
+
+from pylsp import _utils, hookimpl, uris
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py
index 1caab35..759f092 100644
--- a/pylsp/plugins/rope_autoimport.py
+++ b/pylsp/plugins/rope_autoimport.py
@@ -1,6 +1,7 @@
 # Copyright 2022- Python Language Server Contributors.
 
 import logging
+import threading
 from typing import Any, Dict, Generator, List, Optional, Set, Union
 
 import parso
@@ -21,16 +22,78 @@ log = logging.getLogger(__name__)
 
 _score_pow = 5
 _score_max = 10**_score_pow
-MAX_RESULTS = 1000
+MAX_RESULTS_COMPLETIONS = 1000
+MAX_RESULTS_CODE_ACTIONS = 5
+
+
+class AutoimportCache:
+    """Handles the cache creation."""
+
+    def __init__(self):
+        self.thread = None
+
+    def reload_cache(
+        self,
+        config: Config,
+        workspace: Workspace,
+        files: Optional[List[Document]] = None,
+        single_thread: Optional[bool] = True,
+    ):
+        if self.is_blocked():
+            return
+
+        memory: bool = config.plugin_settings("rope_autoimport").get("memory", False)
+        rope_config = config.settings().get("rope", {})
+        autoimport = workspace._rope_autoimport(rope_config, memory)
+        resources: Optional[List[Resource]] = (
+            None
+            if files is None
+            else [document._rope_resource(rope_config) for document in files]
+        )
+
+        if single_thread:
+            self._reload_cache(workspace, autoimport, resources)
+        else:
+            # Creating the cache may take 10-20s for a environment with 5k python modules. That's
+            # why we decided to move cache creation into its own thread.
+            self.thread = threading.Thread(
+                target=self._reload_cache, args=(workspace, autoimport, resources)
+            )
+            self.thread.start()
+
+    def _reload_cache(
+        self,
+        workspace: Workspace,
+        autoimport: AutoImport,
+        resources: Optional[List[Resource]] = None,
+    ):
+        task_handle = PylspTaskHandle(workspace)
+        autoimport.generate_cache(task_handle=task_handle, resources=resources)
+        autoimport.generate_modules_cache(task_handle=task_handle)
+
+    def is_blocked(self):
+        return self.thread and self.thread.is_alive()
 
 
 @hookimpl
 def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]:
     # Default rope_completion to disabled
-    return {"plugins": {"rope_autoimport": {"enabled": False, "memory": False}}}
+    return {
+        "plugins": {
+            "rope_autoimport": {
+                "enabled": False,
+                "memory": False,
+                "completions": {
+                    "enabled": True,
+                },
+                "code_actions": {
+                    "enabled": True,
+                },
+            }
+        }
+    }
 
 
-# pylint: disable=too-many-return-statements
 def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool:
     """
     Check if we should insert the word_node on the given expr.
@@ -122,6 +185,7 @@ def _process_statements(
     word: str,
     autoimport: AutoImport,
     document: Document,
+    feature: str = "completions",
 ) -> Generator[Dict[str, Any], None, None]:
     for suggestion in suggestions:
         insert_line = autoimport.find_insertion_line(document.source) - 1
@@ -134,21 +198,33 @@ def _process_statements(
         if score > _score_max:
             continue
         # TODO make this markdown
-        yield {
-            "label": suggestion.name,
-            "kind": suggestion.itemkind,
-            "sortText": _sort_import(score),
-            "data": {"doc_uri": doc_uri},
-            "detail": _document(suggestion.import_statement),
-            "additionalTextEdits": [edit],
-        }
+        if feature == "completions":
+            yield {
+                "label": suggestion.name,
+                "kind": suggestion.itemkind,
+                "sortText": _sort_import(score),
+                "data": {"doc_uri": doc_uri},
+                "detail": _document(suggestion.import_statement),
+                "additionalTextEdits": [edit],
+            }
+        elif feature == "code_actions":
+            yield {
+                "title": suggestion.import_statement,
+                "kind": "quickfix",
+                "edit": {"changes": {doc_uri: [edit]}},
+                # data is a supported field for codeAction responses
+                # See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction
+                "data": {"sortText": _sort_import(score)},
+            }
+        else:
+            raise ValueError(f"Unknown feature: {feature}")
 
 
 def get_names(script: Script) -> Set[str]:
     """Get all names to ignore from the current file."""
     raw_names = script.get_names(definitions=True)
     log.debug(raw_names)
-    return set(name.name for name in raw_names)
+    return {name.name for name in raw_names}
 
 
 @hookimpl
@@ -160,6 +236,13 @@ def pylsp_completions(
     ignored_names: Union[Set[str], None],
 ):
     """Get autoimport suggestions."""
+    if (
+        not config.plugin_settings("rope_autoimport")
+        .get("completions", {})
+        .get("enabled", True)
+    ) or cache.is_blocked():
+        return []
+
     line = document.lines[position["line"]]
     expr = parso.parse(line)
     word_node = expr.get_leaf_for_position((1, position["character"]))
@@ -173,14 +256,14 @@ def pylsp_completions(
     )
     autoimport = workspace._rope_autoimport(rope_config)
     suggestions = list(autoimport.search_full(word, ignored_names=ignored_names))
-    results = list(
-        sorted(
-            _process_statements(suggestions, document.uri, word, autoimport, document),
-            key=lambda statement: statement["sortText"],
-        )
+    results = sorted(
+        _process_statements(
+            suggestions, document.uri, word, autoimport, document, "completions"
+        ),
+        key=lambda statement: statement["sortText"],
     )
-    if len(results) > MAX_RESULTS:
-        results = results[:MAX_RESULTS]
+    if len(results) > MAX_RESULTS_COMPLETIONS:
+        results = results[:MAX_RESULTS_COMPLETIONS]
     return results
 
 
@@ -193,7 +276,7 @@ def _get_score(
 ) -> int:
     import_length = len("import")
     full_statement_score = len(full_statement) - import_length
-    suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2
+    suggested_name_score = (len(suggested_name) - len(desired_name)) ** 2
     source_score = 20 * source
     return suggested_name_score + full_statement_score + source_score
 
@@ -206,20 +289,79 @@ def _sort_import(score: int) -> str:
     return "[z" + str(score).rjust(_score_pow, "0")
 
 
-def _reload_cache(
-    config: Config, workspace: Workspace, files: Optional[List[Document]] = None
-):
-    memory: bool = config.plugin_settings("rope_autoimport").get("memory", False)
-    rope_config = config.settings().get("rope", {})
-    autoimport = workspace._rope_autoimport(rope_config, memory)
-    task_handle = PylspTaskHandle(workspace)
-    resources: Optional[List[Resource]] = (
-        None
-        if files is None
-        else [document._rope_resource(rope_config) for document in files]
+def get_name_or_module(document, diagnostic) -> str:
+    start = diagnostic["range"]["start"]
+    return (
+        parso.parse(document.lines[start["line"]])
+        .get_leaf_for_position((1, start["character"] + 1))
+        .value
     )
-    autoimport.generate_cache(task_handle=task_handle, resources=resources)
-    autoimport.generate_modules_cache(task_handle=task_handle)
+
+
+@hookimpl
+def pylsp_code_actions(
+    config: Config,
+    workspace: Workspace,
+    document: Document,
+    range: Dict,
+    context: Dict,
+) -> List[Dict]:
+    """
+    Provide code actions through rope.
+
+    Parameters
+    ----------
+    config : pylsp.config.config.Config
+        Current config.
+    workspace : pylsp.workspace.Workspace
+        Current workspace.
+    document : pylsp.workspace.Document
+        Document to apply code actions on.
+    range : Dict
+        Range argument given by pylsp. Not used here.
+    context : Dict
+        CodeActionContext given as dict.
+
+    Returns
+    -------
+      List of dicts containing the code actions.
+    """
+    if (
+        not config.plugin_settings("rope_autoimport")
+        .get("code_actions", {})
+        .get("enabled", True)
+    ) or cache.is_blocked():
+        return []
+
+    log.debug(f"textDocument/codeAction: {document} {range} {context}")
+    code_actions = []
+    for diagnostic in context.get("diagnostics", []):
+        if "undefined name" not in diagnostic.get("message", "").lower():
+            continue
+
+        word = get_name_or_module(document, diagnostic)
+        log.debug(f"autoimport: searching for word: {word}")
+        rope_config = config.settings(document_path=document.path).get("rope", {})
+        autoimport = workspace._rope_autoimport(rope_config)
+        suggestions = list(autoimport.search_full(word))
+        log.debug("autoimport: suggestions: %s", suggestions)
+        results = sorted(
+            _process_statements(
+                suggestions,
+                document.uri,
+                word,
+                autoimport,
+                document,
+                "code_actions",
+            ),
+            key=lambda statement: statement["data"]["sortText"],
+        )
+
+        if len(results) > MAX_RESULTS_CODE_ACTIONS:
+            results = results[:MAX_RESULTS_CODE_ACTIONS]
+        code_actions.extend(results)
+
+    return code_actions
 
 
 @hookimpl
@@ -228,7 +370,7 @@ def pylsp_initialize(config: Config, workspace: Workspace):
 
     Generates the cache for local and global items.
     """
-    _reload_cache(config, workspace)
+    cache.reload_cache(config, workspace)
 
 
 @hookimpl
@@ -237,13 +379,13 @@ def pylsp_document_did_open(config: Config, workspace: Workspace):
 
     Generates the cache for local and global items.
     """
-    _reload_cache(config, workspace)
+    cache.reload_cache(config, workspace)
 
 
 @hookimpl
 def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document):
     """Update the names associated with this document."""
-    _reload_cache(config, workspace, [document])
+    cache.reload_cache(config, workspace, [document])
 
 
 @hookimpl
@@ -255,6 +397,9 @@ def pylsp_workspace_configuration_changed(config: Config, workspace: Workspace):
     Generates the cache for local and global items.
     """
     if config.plugin_settings("rope_autoimport").get("enabled", False):
-        _reload_cache(config, workspace)
+        cache.reload_cache(config, workspace)
     else:
         log.debug("autoimport: Skipping cache reload.")
+
+
+cache: AutoimportCache = AutoimportCache()
diff --git a/pylsp/plugins/rope_completion.py b/pylsp/plugins/rope_completion.py
index ca0d434..b3a1f06 100644
--- a/pylsp/plugins/rope_completion.py
+++ b/pylsp/plugins/rope_completion.py
@@ -2,11 +2,11 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
+
 from rope.contrib.codeassist import code_assist, sorted_proposals
 
 from pylsp import _utils, hookimpl, lsp
 
-
 log = logging.getLogger(__name__)
 
 
@@ -17,7 +17,6 @@ def pylsp_settings():
 
 
 def _resolve_completion(completion, data, markup_kind):
-    # pylint: disable=broad-except
     try:
         doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind)
     except Exception as e:
@@ -30,8 +29,6 @@ def _resolve_completion(completion, data, markup_kind):
 
 @hookimpl
 def pylsp_completions(config, workspace, document, position):
-    # pylint: disable=too-many-locals
-
     settings = config.plugin_settings("rope_completion", document_path=document.path)
     resolve_eagerly = settings.get("eager", False)
 
@@ -63,7 +60,7 @@ def pylsp_completions(config, workspace, document, position):
         definitions = code_assist(
             rope_project, document.source, offset, document_rope, maxfixes=3
         )
-    except Exception as e:  # pylint: disable=broad-except
+    except Exception as e:
         log.debug("Failed to run Rope code assist: %s", e)
         return []
 
diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py
index a4323a4..9e38694 100644
--- a/pylsp/plugins/rope_rename.py
+++ b/pylsp/plugins/rope_rename.py
@@ -6,7 +6,7 @@ import logging
 from rope.base import libutils
 from rope.refactor.rename import Rename
 
-from pylsp import hookimpl, uris, _utils
+from pylsp import _utils, hookimpl, uris
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/plugins/signature.py b/pylsp/plugins/signature.py
index 4fc93df..7ad5b20 100644
--- a/pylsp/plugins/signature.py
+++ b/pylsp/plugins/signature.py
@@ -3,7 +3,8 @@
 
 import logging
 import re
-from pylsp import hookimpl, _utils
+
+from pylsp import _utils, hookimpl
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/plugins/symbols.py b/pylsp/plugins/symbols.py
index e3c961c..4e1890c 100644
--- a/pylsp/plugins/symbols.py
+++ b/pylsp/plugins/symbols.py
@@ -12,12 +12,6 @@ log = logging.getLogger(__name__)
 
 @hookimpl
 def pylsp_document_symbols(config, document):
-    # pylint: disable=broad-except
-    # pylint: disable=too-many-nested-blocks
-    # pylint: disable=too-many-locals
-    # pylint: disable=too-many-branches
-    # pylint: disable=too-many-statements
-
     symbols_settings = config.plugin_settings("jedi_symbols")
     all_scopes = symbols_settings.get("all_scopes", True)
     add_import_symbols = symbols_settings.get("include_import_symbols", True)
@@ -150,7 +144,7 @@ def _container(definition):
         # as children of the module.
         if parent.parent():
             return parent.name
-    except:  # pylint: disable=bare-except
+    except:
         return None
 
     return None
diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py
index 4a8111b..72aa740 100644
--- a/pylsp/plugins/yapf_format.py
+++ b/pylsp/plugins/yapf_format.py
@@ -4,11 +4,10 @@
 import logging
 import os
 
+import whatthepatch
 from yapf.yapflib import file_resources, style
 from yapf.yapflib.yapf_api import FormatCode
 
-import whatthepatch
-
 from pylsp import hookimpl
 from pylsp._utils import get_eol_chars
 
@@ -23,7 +22,7 @@ def pylsp_format_document(workspace, document, options):
 
 
 @hookimpl
-def pylsp_format_range(document, range, options):  # pylint: disable=redefined-builtin
+def pylsp_format_range(document, range, options):
     log.info("Formatting document %s in range %s with yapf", document, range)
     # First we 'round' the range up/down to full lines only
     range["start"]["character"] = 0
diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py
index 760ad97..c606a7c 100644
--- a/pylsp/python_lsp.py
+++ b/pylsp/python_lsp.py
@@ -1,23 +1,23 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-from functools import partial
 import logging
 import os
 import socketserver
 import threading
 import uuid
-from typing import List, Dict, Any
-import ujson as json
+from functools import partial
+from typing import Any, Dict, List
 
+import ujson as json
 from pylsp_jsonrpc.dispatchers import MethodDispatcher
 from pylsp_jsonrpc.endpoint import Endpoint
 from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter
 
-from . import lsp, _utils, uris
-from .config import config
-from .workspace import Workspace, Document, Notebook, Cell
+from . import _utils, lsp, uris
 from ._version import __version__
+from .config import config
+from .workspace import Cell, Document, Notebook, Workspace
 
 log = logging.getLogger(__name__)
 
@@ -45,7 +45,6 @@ class _StreamHandlerWrapper(socketserver.StreamRequestHandler):
             if os.name == "nt":
                 # Catch and pass on ConnectionResetError when parent process
                 # dies
-                # pylint: disable=no-member, undefined-variable
                 if isinstance(e, WindowsError) and e.winerror == 10054:
                     pass
 
@@ -57,7 +56,6 @@ def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class):
         raise ValueError("Handler class must be an instance of PythonLSPServer")
 
     def shutdown_server(check_parent_process, *args):
-        # pylint: disable=unused-argument
         if check_parent_process:
             log.debug("Shutting down server")
             # Shutdown call must be done on a thread, to prevent deadlocks
@@ -103,16 +101,15 @@ def start_ws_lang_server(port, check_parent_process, handler_class):
     if not issubclass(handler_class, PythonLSPServer):
         raise ValueError("Handler class must be an instance of PythonLSPServer")
 
-    # pylint: disable=import-outside-toplevel
-
     # imports needed only for websockets based server
     try:
         import asyncio
         from concurrent.futures import ThreadPoolExecutor
+
         import websockets
     except ImportError as e:
         raise ImportError(
-            "websocket modules missing. Please run pip install 'python-lsp-server[websockets]"
+            "websocket modules missing. Please run: pip install 'python-lsp-server[websockets]'"
         ) from e
 
     with ThreadPoolExecutor(max_workers=10) as tpool:
@@ -135,20 +132,18 @@ def start_ws_lang_server(port, check_parent_process, handler_class):
             async for message in websocket:
                 try:
                     log.debug("consuming payload and feeding it to LSP handler")
-                    # pylint: disable=c-extension-no-member
                     request = json.loads(message)
                     loop = asyncio.get_running_loop()
                     await loop.run_in_executor(tpool, pylsp_handler.consume, request)
-                except Exception as e:  # pylint: disable=broad-except
+                except Exception as e:
                     log.exception("Failed to process request %s, %s", message, str(e))
 
         def send_message(message, websocket):
             """Handler to send responses of  processed requests to respective web socket clients"""
             try:
-                # pylint: disable=c-extension-no-member
                 payload = json.dumps(message, ensure_ascii=False)
                 asyncio.run(websocket.send(payload))
-            except Exception as e:  # pylint: disable=broad-except
+            except Exception as e:
                 log.exception("Failed to write message %s, %s", message, str(e))
 
         async def run_server():
@@ -164,8 +159,6 @@ class PythonLSPServer(MethodDispatcher):
     https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md
     """
 
-    # pylint: disable=too-many-public-methods,redefined-builtin
-
     def __init__(
         self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None
     ):
@@ -385,7 +378,7 @@ class PythonLSPServer(MethodDispatcher):
     def m_initialized(self, **_kwargs):
         self._hook("pylsp_initialized")
 
-    def code_actions(self, doc_uri, range, context):
+    def code_actions(self, doc_uri: str, range: Dict, context: Dict):
         return flatten(
             self._hook("pylsp_code_actions", doc_uri, range=range, context=context)
         )
@@ -454,9 +447,7 @@ class PythonLSPServer(MethodDispatcher):
             doc_uri, flatten(self._hook("pylsp_lint", doc_uri, is_saved=is_saved))
         )
 
-    def _lint_notebook_document(
-        self, notebook_document, workspace
-    ):  # pylint: disable=too-many-locals
+    def _lint_notebook_document(self, notebook_document, workspace):
         """
         Lint a notebook document.
 
@@ -687,7 +678,36 @@ class PythonLSPServer(MethodDispatcher):
     def m_text_document__code_lens(self, textDocument=None, **_kwargs):
         return self.code_lens(textDocument["uri"])
 
+    def _cell_document__completion(self, cellDocument, position=None, **_kwargs):
+        workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
+        notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
+        if notebookDocument is None:
+            raise ValueError("Invalid notebook document")
+
+        cell_data = notebookDocument.cell_data()
+
+        # Concatenate all cells to be a single temporary document
+        total_source = "\n".join(data["source"] for data in cell_data.values())
+        with workspace.temp_document(total_source) as temp_uri:
+            # update position to be the position in the temp document
+            if position is not None:
+                position["line"] += cell_data[cellDocument.uri]["line_start"]
+
+            completions = self.completions(temp_uri, position)
+
+            # Translate temp_uri locations to cell document locations
+            for item in completions.get("items", []):
+                if item.get("data", {}).get("doc_uri") == temp_uri:
+                    item["data"]["doc_uri"] = cellDocument.uri
+
+            return completions
+
     def m_text_document__completion(self, textDocument=None, position=None, **_kwargs):
+        # textDocument here is just a dict with a uri
+        workspace = self._match_uri_to_workspace(textDocument["uri"])
+        document = workspace.get_document(textDocument["uri"])
+        if isinstance(document, Cell):
+            return self._cell_document__completion(document, position, **_kwargs)
         return self.completions(textDocument["uri"], position)
 
     def _cell_document__definition(self, cellDocument, position=None, **_kwargs):
@@ -779,9 +799,7 @@ class PythonLSPServer(MethodDispatcher):
             for doc_uri in workspace.documents:
                 self.lint(doc_uri, is_saved=False)
 
-    def m_workspace__did_change_workspace_folders(
-        self, event=None, **_kwargs
-    ):  # pylint: disable=too-many-locals
+    def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):
         if event is None:
             return
         added = event.get("added", [])
diff --git a/pylsp/uris.py b/pylsp/uris.py
index 7e5c4d0..cba5b29 100644
--- a/pylsp/uris.py
+++ b/pylsp/uris.py
@@ -5,8 +5,10 @@
 
 https://github.com/Microsoft/vscode-uri/blob/e59cab84f5df6265aed18ae5f43552d3eef13bb9/lib/index.ts
 """
+
 import re
 from urllib import parse
+
 from pylsp import IS_WIN
 
 RE_DRIVE_LETTER_PATH = re.compile(r"^\/[a-zA-Z]:")
diff --git a/pylsp/workspace.py b/pylsp/workspace.py
index fb524c7..c1b32f2 100644
--- a/pylsp/workspace.py
+++ b/pylsp/workspace.py
@@ -1,19 +1,19 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
+import functools
 import io
 import logging
-from contextlib import contextmanager
 import os
 import re
 import uuid
-import functools
-from typing import Optional, Generator, Callable, List
+from contextlib import contextmanager
 from threading import RLock
+from typing import Callable, Generator, List, Optional
 
 import jedi
 
-from . import lsp, uris, _utils
+from . import _utils, lsp, uris
 
 log = logging.getLogger(__name__)
 
@@ -36,8 +36,6 @@ def lock(method):
 
 
 class Workspace:
-    # pylint: disable=too-many-public-methods
-
     M_PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics"
     M_PROGRESS = "$/progress"
     M_INITIALIZE_PROGRESS = "window/workDoneProgress/create"
@@ -60,8 +58,11 @@ class Workspace:
         self.__rope_config = None
         self.__rope_autoimport = None
 
-    def _rope_autoimport(self, rope_config: Optional, memory: bool = False):
-        # pylint: disable=import-outside-toplevel
+    def _rope_autoimport(
+        self,
+        rope_config: Optional,
+        memory: bool = False,
+    ):
         from rope.contrib.autoimport.sqlite import AutoImport
 
         if self.__rope_autoimport is None:
@@ -70,7 +71,6 @@ class Workspace:
         return self.__rope_autoimport
 
     def _rope_project_builder(self, rope_config):
-        # pylint: disable=import-outside-toplevel
         from rope.base.project import Project
 
         # TODO: we could keep track of dirty files and validate only those
@@ -232,7 +232,6 @@ class Workspace:
         def dummy_progress_message(
             message: str, percentage: Optional[int] = None
         ) -> None:
-            # pylint: disable=unused-argument
             pass
 
         yield dummy_progress_message
@@ -251,7 +250,7 @@ class Workspace:
                 self._endpoint.request(
                     self.M_INITIALIZE_PROGRESS, {"token": token}
                 ).result(timeout=1.0)
-            except Exception:  # pylint: disable=broad-exception-caught
+            except Exception:
                 log.warning(
                     "There was an error while trying to initialize progress reporting."
                     "Likely progress reporting was used in a synchronous LSP handler, "
@@ -374,7 +373,7 @@ class Workspace:
         )
 
     def close(self):
-        if self.__rope_autoimport is not None:
+        if self.__rope_autoimport:
             self.__rope_autoimport.close()
 
 
@@ -408,7 +407,6 @@ class Document:
         return str(self.uri)
 
     def _rope_resource(self, rope_config):
-        # pylint: disable=import-outside-toplevel
         from rope.base import libutils
 
         return libutils.path_to_resource(
@@ -649,7 +647,7 @@ class Notebook:
             names.update(cell_document.jedi_names(all_scopes, definitions, references))
             if cell_uri == up_to_cell_uri:
                 break
-        return set(name.name for name in names)
+        return {name.name for name in names}
 
 
 class Cell(Document):
diff --git a/pyproject.toml b/pyproject.toml
index d2983ed..4665dcb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,28 +28,28 @@ Homepage = "https://github.com/python-lsp/python-lsp-server"
 [project.optional-dependencies]
 all = [
     "autopep8>=2.0.4,<2.1.0",
-    "flake8>=6.1.0,<7",
+    "flake8>=7,<8",
     "mccabe>=0.7.0,<0.8.0",
     "pycodestyle>=2.11.0,<2.12.0",
     "pydocstyle>=6.3.0,<6.4.0",
-    "pyflakes>=3.1.0,<3.2.0",
-    "pylint>=2.5.0,<3",
-    "rope>1.2.0",
+    "pyflakes>=3.2.0,<3.3.0",
+    "pylint>=2.5.0,<3.1",
+    "rope>=1.11.0",
     "yapf>=0.33.0",
     "whatthepatch>=1.0.2,<2.0.0"
 ]
-autopep8 = ["autopep8>=1.6.0,<2.1.0"]
-flake8 = ["flake8>=6.1.0,<7"]
+autopep8 = ["autopep8>=2.0.4,<2.1.0"]
+flake8 = ["flake8>=7,<8"]
 mccabe = ["mccabe>=0.7.0,<0.8.0"]
 pycodestyle = ["pycodestyle>=2.11.0,<2.12.0"]
 pydocstyle = ["pydocstyle>=6.3.0,<6.4.0"]
-pyflakes = ["pyflakes>=3.1.0,<3.2.0"]
-pylint = ["pylint>=2.5.0,<3"]
-rope = ["rope>1.2.0"]
+pyflakes = ["pyflakes>=3.2.0,<3.3.0"]
+pylint = ["pylint>=2.5.0,<3.1"]
+rope = ["rope>=1.11.0"]
 yapf = ["yapf>=0.33.0", "whatthepatch>=1.0.2,<2.0.0"]
 websockets = ["websockets>=10.3"]
 test = [
-    "pylint>=2.5.0,<3",
+    "pylint>=2.5.0,<3.1",
     "pytest",
     "pytest-cov",
     "coverage",
@@ -86,6 +86,87 @@ yapf = "pylsp.plugins.yapf_format"
 [project.scripts]
 pylsp = "pylsp.__main__:main"
 
+[tool.ruff]
+# Exclude a variety of commonly ignored directories.
+exclude = [
+    ".bzr",
+    ".direnv",
+    ".eggs",
+    ".git",
+    ".git-rewrite",
+    ".hg",
+    ".ipynb_checkpoints",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pyenv",
+    ".pytest_cache",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    ".vscode",
+    "__pypackages__",
+    "_build",
+    "buck-out",
+    "build",
+    "dist",
+    "node_modules",
+    "site-packages",
+    "venv",
+]
+
+# Same as Black.
+line-length = 88
+indent-width = 4
+
+# Assume Python 3.8
+target-version = "py38"
+
+[tool.ruff.lint]
+# https://docs.astral.sh/ruff/rules/
+select = ["E", "F", "W", "C", "I"]
+ignore = [
+    "C901",  # McCabe complexity warning
+    "E501",  # Line too long
+    "E722",  # Do not use bare `except`
+]
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
+
+# Enable auto-formatting of code examples in docstrings. Markdown,
+# reStructuredText code/literal blocks and doctests are all supported.
+#
+# This is currently disabled by default, but it is planned for this
+# to be opt-out in the future.
+docstring-code-format = false
+
+# Set the line length limit used when formatting code snippets in
+# docstrings.
+#
+# This only has an effect when the `docstring-code-format` setting is
+# enabled.
+docstring-code-line-length = "dynamic"
+
 [tool.setuptools]
 license-files = ["LICENSE"]
 include-package-data = false
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index b606d41..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright 2017-2020 Palantir Technologies, Inc.
-# Copyright 2021- Python Language Server Contributors.
-
-[pycodestyle]
-ignore = E203, E226, E722, W503, W504
-max-line-length = 120
-exclude = test/plugins/.ropeproject,test/.ropeproject
diff --git a/setup.py b/setup.py
index b419c0f..762deb8 100755
--- a/setup.py
+++ b/setup.py
@@ -3,8 +3,7 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-from setuptools import setup, find_packages
-
+from setuptools import setup
 
 if __name__ == "__main__":
     setup(


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
index bb21669..0936e78 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -2,8 +2,8 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import pytest
-from pylsp import IS_WIN
 
+from pylsp import IS_WIN
 
 unix_only = pytest.mark.skipif(IS_WIN, reason="Unix only")
 windows_only = pytest.mark.skipif(not IS_WIN, reason="Windows only")
diff --git a/test/conftest.py b/test/conftest.py
index 332866c..a901051 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,8 +1,10 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-""" py.test configuration"""
+"""pytest configuration"""
+
 import logging
+
 from pylsp.__main__ import LOG_FORMAT
 
 logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
diff --git a/test/fixtures.py b/test/fixtures.py
index ed6206a..81a8b08 100644
--- a/test/fixtures.py
+++ b/test/fixtures.py
@@ -3,13 +3,10 @@
 
 import os
 from io import StringIO
+from test.test_utils import CALL_TIMEOUT_IN_SECONDS, ClientServerPair
 from unittest.mock import MagicMock
 
-from test.test_utils import ClientServerPair, CALL_TIMEOUT_IN_SECONDS
-
 import pytest
-import pylsp_jsonrpc
-
 from pylsp_jsonrpc.dispatchers import MethodDispatcher
 from pylsp_jsonrpc.endpoint import Endpoint
 from pylsp_jsonrpc.exceptions import JsonRpcException
@@ -17,8 +14,7 @@ from pylsp_jsonrpc.exceptions import JsonRpcException
 from pylsp import uris
 from pylsp.config.config import Config
 from pylsp.python_lsp import PythonLSPServer
-from pylsp.workspace import Workspace, Document
-
+from pylsp.workspace import Document, Workspace
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
@@ -108,7 +104,7 @@ def consumer():
 
 
 @pytest.fixture()
-def endpoint(consumer):  # pylint: disable=redefined-outer-name
+def endpoint(consumer):
     class Dispatcher(FakeEditorMethodsMixin, MethodDispatcher):
         pass
 
@@ -116,7 +112,7 @@ def endpoint(consumer):  # pylint: disable=redefined-outer-name
 
 
 @pytest.fixture
-def workspace(tmpdir, endpoint):  # pylint: disable=redefined-outer-name
+def workspace(tmpdir, endpoint):
     """Return a workspace."""
     ws = Workspace(uris.from_fs_path(str(tmpdir)), endpoint)
     ws._config = Config(ws.root_uri, {}, 0, {})
@@ -125,7 +121,7 @@ def workspace(tmpdir, endpoint):  # pylint: disable=redefined-outer-name
 
 
 @pytest.fixture
-def workspace_other_root_path(tmpdir, endpoint):  # pylint: disable=redefined-outer-name
+def workspace_other_root_path(tmpdir, endpoint):
     """Return a workspace with a root_path other than tmpdir."""
     ws_path = str(tmpdir.mkdir("test123").mkdir("test456"))
     ws = Workspace(uris.from_fs_path(ws_path), endpoint)
@@ -134,7 +130,7 @@ def workspace_other_root_path(tmpdir, endpoint):  # pylint: disable=redefined-ou
 
 
 @pytest.fixture
-def config(workspace):  # pylint: disable=redefined-outer-name
+def config(workspace):
     """Return a config object."""
     cfg = Config(workspace.root_uri, {}, 0, {})
     cfg._plugin_settings = {
@@ -144,12 +140,12 @@ def config(workspace):  # pylint: disable=redefined-outer-name
 
 
 @pytest.fixture
-def doc(workspace):  # pylint: disable=redefined-outer-name
+def doc(workspace):
     return Document(DOC_URI, workspace, DOC)
 
 
 @pytest.fixture
-def temp_workspace_factory(workspace):  # pylint: disable=redefined-outer-name
+def temp_workspace_factory(workspace):
     """
     Returns a function that creates a temporary workspace from the files dict.
     The dict is in the format {"file_name": "file_contents"}
@@ -176,13 +172,8 @@ def client_server_pair():
 
     yield (client_server_pair_obj.client, client_server_pair_obj.server)
 
-    try:
-        shutdown_response = client_server_pair_obj.client._endpoint.request(
-            "shutdown"
-        ).result(timeout=CALL_TIMEOUT_IN_SECONDS)
-        assert shutdown_response is None
-        client_server_pair_obj.client._endpoint.notify("exit")
-    except pylsp_jsonrpc.exceptions.JsonRpcInvalidParams:
-        # SQLite objects created in a thread can only be used in that same thread.
-        # This exeception is raised when testing rope autoimport.
-        client_server_pair_obj.client._endpoint.notify("exit")
+    shutdown_response = client_server_pair_obj.client._endpoint.request(
+        "shutdown"
+    ).result(timeout=CALL_TIMEOUT_IN_SECONDS)
+    assert shutdown_response is None
+    client_server_pair_obj.client._endpoint.notify("exit")
diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py
index b1c4677..4ac635a 100644
--- a/test/plugins/test_autoimport.py
+++ b/test/plugins/test_autoimport.py
@@ -1,10 +1,9 @@
 # Copyright 2022- Python Language Server Contributors.
 
-from typing import Any, Dict, List
-from unittest.mock import Mock, patch
-
 from test.test_notebook_document import wait_for_condition
 from test.test_utils import send_initialize_request, send_notebook_did_open
+from typing import Any, Dict, List
+from unittest.mock import Mock, patch
 
 import jedi
 import parso
@@ -12,24 +11,33 @@ import pytest
 
 from pylsp import IS_WIN, lsp, uris
 from pylsp.config.config import Config
-from pylsp.plugins.rope_autoimport import _get_score, _should_insert, get_names
+from pylsp.plugins.rope_autoimport import (
+    _get_score,
+    _should_insert,
+    cache,
+    get_name_or_module,
+    get_names,
+)
 from pylsp.plugins.rope_autoimport import (
     pylsp_completions as pylsp_autoimport_completions,
 )
-from pylsp.plugins.rope_autoimport import pylsp_initialize
 from pylsp.workspace import Workspace
 
-
 DOC_URI = uris.from_fs_path(__file__)
 
 
-def contains_autoimport(suggestion: Dict[str, Any], module: str) -> bool:
-    """Checks if `suggestion` contains an autoimport for `module`."""
+def contains_autoimport_completion(suggestion: Dict[str, Any], module: str) -> bool:
+    """Checks if `suggestion` contains an autoimport completion for `module`."""
     return suggestion.get("label", "") == module and "import" in suggestion.get(
         "detail", ""
     )
 
 
+def contains_autoimport_quickfix(suggestion: Dict[str, Any], module: str) -> bool:
+    """Checks if `suggestion` contains an autoimport quick fix for `module`."""
+    return suggestion.get("title", "") == f"import {module}"
+
+
 @pytest.fixture(scope="session")
 def autoimport_workspace(tmp_path_factory) -> Workspace:
     "Special autoimport workspace. Persists across sessions to make in-memory sqlite3 database fast."
@@ -37,13 +45,21 @@ def autoimport_workspace(tmp_path_factory) -> Workspace:
         uris.from_fs_path(str(tmp_path_factory.mktemp("pylsp"))), Mock()
     )
     workspace._config = Config(workspace.root_uri, {}, 0, {})
-    workspace._config.update({"rope_autoimport": {"memory": True, "enabled": True}})
-    pylsp_initialize(workspace._config, workspace)
+    workspace._config.update(
+        {
+            "rope_autoimport": {
+                "memory": True,
+                "enabled": True,
+                "completions": {"enabled": True},
+                "code_actions": {"enabled": True},
+            }
+        }
+    )
+    cache.reload_cache(workspace._config, workspace, single_thread=True)
     yield workspace
     workspace.close()
 
 
-# pylint: disable=redefined-outer-name
 @pytest.fixture
 def completions(config: Config, autoimport_workspace: Workspace, request):
     document, position = request.param
@@ -213,16 +229,67 @@ def test_get_names():
         sfiosifo
     """
     results = get_names(jedi.Script(code=source))
-    assert results == set(["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"])
+    assert results == {"blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"}
+
+
+# Tests ruff, flake8 and pyflakes messages
+@pytest.mark.parametrize(
+    "message",
+    ["Undefined name `os`", "F821 undefined name 'numpy'", "undefined name 'numpy'"],
+)
+def test_autoimport_code_actions_get_correct_module_name(autoimport_workspace, message):
+    source = "os.path.join('a', 'b')"
+    autoimport_workspace.put_document(DOC_URI, source=source)
+    doc = autoimport_workspace.get_document(DOC_URI)
+    diagnostic = {
+        "range": {
+            "start": {"line": 0, "character": 0},
+            "end": {"line": 0, "character": 2},
+        },
+        "message": message,
+    }
+    module_name = get_name_or_module(doc, diagnostic)
+    autoimport_workspace.rm_document(DOC_URI)
+    assert module_name == "os"
+
+
+def make_context(module_name, line, character_start, character_end):
+    return {
+        "diagnostics": [
+            {
+                "message": f"undefined name '{module_name}'",
+                "range": {
+                    "start": {"line": line, "character": character_start},
+                    "end": {"line": line, "character": character_end},
+                },
+            }
+        ]
+    }
+
+
+def position(line, character):
+    return {"line": line, "character": character}
 
 
 @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows")
-def test_autoimport_for_notebook_document(
+def test_autoimport_code_actions_and_completions_for_notebook_document(
     client_server_pair,
 ):
     client, server = client_server_pair
-    send_initialize_request(client)
-
+    send_initialize_request(
+        client,
+        {
+            "pylsp": {
+                "plugins": {
+                    "rope_autoimport": {
+                        "memory": True,
+                        "enabled": True,
+                        "completions": {"enabled": True},
+                    },
+                }
+            }
+        },
+    )
     with patch.object(server._endpoint, "notify") as mock_notify:
         # Expectations:
         # 1. We receive an autoimport suggestion for "os" in the first cell because
@@ -231,57 +298,49 @@ def test_autoimport_for_notebook_document(
         #    already imported in the second cell.
         # 3. We don't receive an autoimport suggestion for "os" in the third cell because it's
         #    already imported in the second cell.
-        # 4. We receive an autoimport suggestion for "sys" because it's not already imported
+        # 4. We receive an autoimport suggestion for "sys" because it's not already imported.
+        # 5. If diagnostics doesn't contain "undefined name ...", we send empty quick fix suggestions.
         send_notebook_did_open(client, ["os", "import os\nos", "os", "sys"])
-        wait_for_condition(lambda: mock_notify.call_count >= 3)
+        wait_for_condition(lambda: mock_notify.call_count >= 4)
+        # We received diagnostics messages for every cell
+        assert all(
+            "textDocument/publishDiagnostics" in c.args
+            for c in mock_notify.call_args_list
+        )
 
-    server.m_workspace__did_change_configuration(
-        settings={
-            "pylsp": {"plugins": {"rope_autoimport": {"enabled": True, "memory": True}}}
-        }
-    )
     rope_autoimport_settings = server.workspace._config.plugin_settings(
         "rope_autoimport"
     )
-    assert rope_autoimport_settings.get("enabled", False) is True
+    assert rope_autoimport_settings.get("completions", {}).get("enabled", False) is True
     assert rope_autoimport_settings.get("memory", False) is True
+    wait_for_condition(lambda: not cache.is_blocked())
 
     # 1.
-    suggestions = server.completions("cell_1_uri", {"line": 0, "character": 2}).get(
-        "items"
-    )
-    assert any(
-        suggestion
-        for suggestion in suggestions
-        if contains_autoimport(suggestion, "os")
-    )
+    quick_fixes = server.code_actions("cell_1_uri", {}, make_context("os", 0, 0, 2))
+    assert any(s for s in quick_fixes if contains_autoimport_quickfix(s, "os"))
+
+    completions = server.completions("cell_1_uri", position(0, 2)).get("items")
+    assert any(s for s in completions if contains_autoimport_completion(s, "os"))
 
     # 2.
-    suggestions = server.completions("cell_2_uri", {"line": 1, "character": 2}).get(
-        "items"
-    )
-    assert not any(
-        suggestion
-        for suggestion in suggestions
-        if contains_autoimport(suggestion, "os")
-    )
+    # We don't test code actions here as in this case, there would be no code actions sent bc
+    # there wouldn't be a diagnostics message.
+    completions = server.completions("cell_2_uri", position(1, 2)).get("items")
+    assert not any(s for s in completions if contains_autoimport_completion(s, "os"))
 
     # 3.
-    suggestions = server.completions("cell_3_uri", {"line": 0, "character": 2}).get(
-        "items"
-    )
-    assert not any(
-        suggestion
-        for suggestion in suggestions
-        if contains_autoimport(suggestion, "os")
-    )
+    # Same as in 2.
+    completions = server.completions("cell_3_uri", position(0, 2)).get("items")
+    assert not any(s for s in completions if contains_autoimport_completion(s, "os"))
 
     # 4.
-    suggestions = server.completions("cell_4_uri", {"line": 0, "character": 3}).get(
-        "items"
-    )
-    assert any(
-        suggestion
-        for suggestion in suggestions
-        if contains_autoimport(suggestion, "sys")
-    )
+    quick_fixes = server.code_actions("cell_4_uri", {}, make_context("sys", 0, 0, 3))
+    assert any(s for s in quick_fixes if contains_autoimport_quickfix(s, "sys"))
+
+    completions = server.completions("cell_4_uri", position(0, 3)).get("items")
+    assert any(s for s in completions if contains_autoimport_completion(s, "sys"))
+
+    # 5.
+    context = {"diagnostics": [{"message": "A random message"}]}
+    quick_fixes = server.code_actions("cell_4_uri", {}, context)
+    assert len(quick_fixes) == 0
diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py
index 00a54eb..4b598be 100644
--- a/test/plugins/test_completion.py
+++ b/test/plugins/test_completion.py
@@ -4,21 +4,19 @@
 import math
 import os
 import sys
-
 from pathlib import Path
-from typing import NamedTuple, Dict
+from typing import Dict, NamedTuple
 
 import pytest
 
-from pylsp import uris, lsp
-from pylsp.workspace import Document
-from pylsp.plugins.jedi_completion import pylsp_completions as pylsp_jedi_completions
+from pylsp import lsp, uris
+from pylsp._utils import JEDI_VERSION
 from pylsp.plugins.jedi_completion import (
     pylsp_completion_item_resolve as pylsp_jedi_completion_item_resolve,
 )
+from pylsp.plugins.jedi_completion import pylsp_completions as pylsp_jedi_completions
 from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions
-from pylsp._utils import JEDI_VERSION
-
+from pylsp.workspace import Document
 
 PY2 = sys.version[0] == "2"
 LINUX = sys.platform.startswith("linux")
@@ -583,9 +581,18 @@ def test_file_completions(workspace, tmpdir):
     # Check completions
     assert len(completions) == 2
     assert [c["kind"] == lsp.CompletionItemKind.File for c in completions]
-    assert (
-        completions[0]["insertText"] == ("bar" + "\\\\")
-        if os.name == "nt"
-        else ("bar" + "\\/")
+    assert completions[0]["insertText"] == (
+        ("bar" + "\\") if os.name == "nt" else ("bar" + "/")
+    )
+    assert completions[1]["insertText"] == 'foo.txt"'
+
+    # When snippets are supported, ensure that path separators are escaped.
+    support_snippet = {
+        "textDocument": {"completion": {"completionItem": {"snippetSupport": True}}}
+    }
+    doc._config.capabilities.update(support_snippet)
+    completions = pylsp_jedi_completions(doc._config, doc, com_position)
+    assert completions[0]["insertText"] == (
+        ("bar" + "\\\\") if os.name == "nt" else ("bar" + "\\/")
     )
     assert completions[1]["insertText"] == 'foo.txt"'
diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py
index c366e8c..5da04ff 100644
--- a/test/plugins/test_definitions.py
+++ b/test/plugins/test_definitions.py
@@ -7,7 +7,6 @@ from pylsp import uris
 from pylsp.plugins.definition import pylsp_definitions
 from pylsp.workspace import Document
 
-
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """def a():
     pass
@@ -21,8 +20,8 @@ class Directory(object):
 
     def add_member(self, id, name):
         self.members[id] = name
-        
-        
+
+
 subscripted_before_reference = {}
 subscripted_before_reference[0] = 0
 subscripted_before_reference
diff --git a/test/plugins/test_flake8_lint.py b/test/plugins/test_flake8_lint.py
index 882bc99..bdc180f 100644
--- a/test/plugins/test_flake8_lint.py
+++ b/test/plugins/test_flake8_lint.py
@@ -1,15 +1,15 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-import tempfile
 import os
+import tempfile
 from textwrap import dedent
 from unittest.mock import patch
+
 from pylsp import lsp, uris
 from pylsp.plugins import flake8_lint
 from pylsp.workspace import Document
 
-
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """import pylsp
 
@@ -40,7 +40,7 @@ def test_flake8_unsaved(workspace):
     assert unused_var["code"] == "F841"
     assert unused_var["range"]["start"] == {"line": 5, "character": 1}
     assert unused_var["range"]["end"] == {"line": 5, "character": 11}
-    assert unused_var["severity"] == lsp.DiagnosticSeverity.Error
+    assert unused_var["severity"] == lsp.DiagnosticSeverity.Warning
     assert unused_var["tags"] == [lsp.DiagnosticTag.Unnecessary]
 
 
@@ -55,7 +55,7 @@ def test_flake8_lint(workspace):
         assert unused_var["code"] == "F841"
         assert unused_var["range"]["start"] == {"line": 5, "character": 1}
         assert unused_var["range"]["end"] == {"line": 5, "character": 11}
-        assert unused_var["severity"] == lsp.DiagnosticSeverity.Error
+        assert unused_var["severity"] == lsp.DiagnosticSeverity.Warning
     finally:
         os.remove(name)
 
@@ -101,7 +101,7 @@ def test_flake8_respecting_configuration(workspace):
                 "end": {"line": 5, "character": 11},
             },
             "message": "F841 local variable 'a' is assigned to but never used",
-            "severity": 1,
+            "severity": 2,
             "tags": [1],
         },
     ]
@@ -116,7 +116,7 @@ def test_flake8_respecting_configuration(workspace):
                 "end": {"line": 0, "character": 9},
             },
             "message": "F401 'os' imported but unused",
-            "severity": 1,
+            "severity": 2,
             "tags": [1],
         }
     ]
diff --git a/test/plugins/test_highlight.py b/test/plugins/test_highlight.py
index e7bd507..fcc9809 100644
--- a/test/plugins/test_highlight.py
+++ b/test/plugins/test_highlight.py
@@ -2,9 +2,8 @@
 # Copyright 2021- Python Language Server Contributors.
 
 from pylsp import lsp, uris
-from pylsp.workspace import Document
 from pylsp.plugins.highlight import pylsp_document_highlight
-
+from pylsp.workspace import Document
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """a = "hello"
diff --git a/test/plugins/test_hover.py b/test/plugins/test_hover.py
index 7049bec..a65f92d 100644
--- a/test/plugins/test_hover.py
+++ b/test/plugins/test_hover.py
@@ -59,7 +59,6 @@ def test_numpy_hover(workspace):
     )
 
     # https://github.com/davidhalter/jedi/issues/1746
-    # pylint: disable=import-outside-toplevel
     import numpy as np
 
     if np.lib.NumpyVersion(np.__version__) < "1.20.0":
diff --git a/test/plugins/test_jedi_rename.py b/test/plugins/test_jedi_rename.py
index d88a929..753303d 100644
--- a/test/plugins/test_jedi_rename.py
+++ b/test/plugins/test_jedi_rename.py
@@ -4,11 +4,11 @@
 import os
 
 import pytest
+
 from pylsp import uris
 from pylsp.plugins.jedi_rename import pylsp_rename
 from pylsp.workspace import Document
 
-
 DOC_NAME = "test1.py"
 DOC = """class Test1():
     pass
@@ -33,7 +33,7 @@ def tmp_workspace(temp_workspace_factory):
     )
 
 
-def test_jedi_rename(tmp_workspace, config):  # pylint: disable=redefined-outer-name
+def test_jedi_rename(tmp_workspace, config):
     # rename the `Test1` class
     position = {"line": 0, "character": 6}
     DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME))
diff --git a/test/plugins/test_mccabe_lint.py b/test/plugins/test_mccabe_lint.py
index 009d8cf..6c420b1 100644
--- a/test/plugins/test_mccabe_lint.py
+++ b/test/plugins/test_mccabe_lint.py
@@ -2,8 +2,8 @@
 # Copyright 2021- Python Language Server Contributors.
 
 from pylsp import lsp, uris
-from pylsp.workspace import Document
 from pylsp.plugins import mccabe_lint
+from pylsp.workspace import Document
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """def hello():
diff --git a/test/plugins/test_pycodestyle_lint.py b/test/plugins/test_pycodestyle_lint.py
index dd24daa..5d83648 100644
--- a/test/plugins/test_pycodestyle_lint.py
+++ b/test/plugins/test_pycodestyle_lint.py
@@ -6,8 +6,8 @@ import os
 import pytest
 
 from pylsp import lsp, uris
-from pylsp.workspace import Document
 from pylsp.plugins import pycodestyle_lint
+from pylsp.workspace import Document
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
diff --git a/test/plugins/test_pydocstyle_lint.py b/test/plugins/test_pydocstyle_lint.py
index df352da..22f10d0 100644
--- a/test/plugins/test_pydocstyle_lint.py
+++ b/test/plugins/test_pydocstyle_lint.py
@@ -2,9 +2,10 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import os
+
 from pylsp import lsp, uris
-from pylsp.workspace import Document
 from pylsp.plugins import pydocstyle_lint
+from pylsp.workspace import Document
 
 DOC_URI = uris.from_fs_path(os.path.join(os.path.dirname(__file__), "pydocstyle.py"))
 TEST_DOC_URI = uris.from_fs_path(__file__)
diff --git a/test/plugins/test_pyflakes_lint.py b/test/plugins/test_pyflakes_lint.py
index e33457f..6ad774c 100644
--- a/test/plugins/test_pyflakes_lint.py
+++ b/test/plugins/test_pyflakes_lint.py
@@ -4,8 +4,8 @@
 import sys
 
 from pylsp import lsp, uris
-from pylsp.workspace import Document
 from pylsp.plugins import pyflakes_lint
+from pylsp.workspace import Document
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
diff --git a/test/plugins/test_pylint_lint.py b/test/plugins/test_pylint_lint.py
index 210e1cf..8ce10eb 100644
--- a/test/plugins/test_pylint_lint.py
+++ b/test/plugins/test_pylint_lint.py
@@ -3,13 +3,13 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import contextlib
-from pathlib import Path
 import os
 import tempfile
+from pathlib import Path
 
 from pylsp import lsp, uris
-from pylsp.workspace import Document, Workspace
 from pylsp.plugins import pylint_lint
+from pylsp.workspace import Document, Workspace
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """import sys
diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py
index e8030a4..6990317 100644
--- a/test/plugins/test_references.py
+++ b/test/plugins/test_references.py
@@ -6,9 +6,8 @@ import os
 import pytest
 
 from pylsp import uris
-from pylsp.workspace import Document
 from pylsp.plugins.references import pylsp_references
-
+from pylsp.workspace import Document
 
 DOC1_NAME = "test1.py"
 DOC2_NAME = "test2.py"
@@ -36,7 +35,7 @@ def tmp_workspace(temp_workspace_factory):
     )
 
 
-def test_references(tmp_workspace):  # pylint: disable=redefined-outer-name
+def test_references(tmp_workspace):
     # Over 'Test1' in class Test1():
     position = {"line": 0, "character": 8}
     DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME))
@@ -66,7 +65,7 @@ def test_references(tmp_workspace):  # pylint: disable=redefined-outer-name
     assert doc2_usage_ref["range"]["end"] == {"line": 3, "character": 9}
 
 
-def test_references_builtin(tmp_workspace):  # pylint: disable=redefined-outer-name
+def test_references_builtin(tmp_workspace):
     # Over 'UnicodeError':
     position = {"line": 4, "character": 7}
     doc2_uri = uris.from_fs_path(os.path.join(str(tmp_workspace.root_path), DOC2_NAME))
diff --git a/test/plugins/test_rope_rename.py b/test/plugins/test_rope_rename.py
index 9b9039b..c55ead0 100644
--- a/test/plugins/test_rope_rename.py
+++ b/test/plugins/test_rope_rename.py
@@ -4,11 +4,11 @@
 import os
 
 import pytest
+
 from pylsp import uris
 from pylsp.plugins.rope_rename import pylsp_rename
 from pylsp.workspace import Document
 
-
 DOC_NAME = "test1.py"
 DOC = """class Test1():
     pass
@@ -26,7 +26,7 @@ def tmp_workspace(temp_workspace_factory):
     return temp_workspace_factory({DOC_NAME: DOC, DOC_NAME_SIMPLE: DOC_SIMPLE})
 
 
-def test_rope_rename(tmp_workspace, config):  # pylint: disable=redefined-outer-name
+def test_rope_rename(tmp_workspace, config):
     position = {"line": 0, "character": 6}
     DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME))
     doc = Document(DOC_URI, tmp_workspace)
diff --git a/test/plugins/test_signature.py b/test/plugins/test_signature.py
index 0ba28ac..7c28572 100644
--- a/test/plugins/test_signature.py
+++ b/test/plugins/test_signature.py
@@ -2,6 +2,7 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import pytest
+
 from pylsp import uris
 from pylsp.plugins import signature
 from pylsp.workspace import Document
diff --git a/test/plugins/test_symbols.py b/test/plugins/test_symbols.py
index 0f54e9d..4dc74bc 100644
--- a/test/plugins/test_symbols.py
+++ b/test/plugins/test_symbols.py
@@ -7,11 +7,10 @@ import sys
 import pytest
 
 from pylsp import uris
-from pylsp.plugins.symbols import pylsp_document_symbols
 from pylsp.lsp import SymbolKind
+from pylsp.plugins.symbols import pylsp_document_symbols
 from pylsp.workspace import Document
 
-
 PY2 = sys.version[0] == "2"
 LINUX = sys.platform.startswith("linux")
 CI = os.environ.get("CI")
diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py
index d89eb37..fddd68b 100644
--- a/test/plugins/test_yapf_format.py
+++ b/test/plugins/test_yapf_format.py
@@ -5,8 +5,8 @@ import pytest
 
 from pylsp import uris
 from pylsp.plugins.yapf_format import pylsp_format_document, pylsp_format_range
-from pylsp.workspace import Document
 from pylsp.text_edit import apply_text_edits
+from pylsp.workspace import Document
 
 DOC_URI = uris.from_fs_path(__file__)
 DOC = """A = [
diff --git a/test/test_configuration.py b/test/test_configuration.py
index 91da421..ddc6315 100644
--- a/test/test_configuration.py
+++ b/test/test_configuration.py
@@ -1,15 +1,13 @@
 # Copyright 2021- Python Language Server Contributors.
 
-from unittest.mock import patch
-
-from test.test_utils import send_initialize_request
 from test.test_notebook_document import wait_for_condition
+from test.test_utils import send_initialize_request
+from unittest.mock import patch
 
 import pytest
 
 from pylsp import IS_WIN
 
-
 INITIALIZATION_OPTIONS = {
     "pylsp": {
         "plugins": {
diff --git a/test/test_document.py b/test/test_document.py
index 1acb161..7caa0ab 100644
--- a/test/test_document.py
+++ b/test/test_document.py
@@ -1,7 +1,8 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-from test.fixtures import DOC_URI, DOC
+from test.fixtures import DOC, DOC_URI
+
 from pylsp.workspace import Document
 
 
diff --git a/test/test_language_server.py b/test/test_language_server.py
index 401b1ce..6a48638 100644
--- a/test/test_language_server.py
+++ b/test/test_language_server.py
@@ -2,15 +2,13 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import os
-import time
 import sys
-
+import time
 from test.test_utils import ClientServerPair, send_initialize_request
 
+import pytest
 from flaky import flaky
 from pylsp_jsonrpc.exceptions import JsonRpcMethodNotFound
-import pytest
-
 
 RUNNING_IN_CI = bool(os.environ.get("CI"))
 
@@ -46,7 +44,7 @@ def test_initialize(client_server_pair):
 )
 def test_exit_with_parent_process_died(
     client_exited_server,
-):  # pylint: disable=redefined-outer-name
+):
     # language server should have already exited before responding
     lsp_server, mock_process = (
         client_exited_server.client,
diff --git a/test/test_notebook_document.py b/test/test_notebook_document.py
index c63d279..c1ac198 100644
--- a/test/test_notebook_document.py
+++ b/test/test_notebook_document.py
@@ -1,19 +1,18 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import time
-from unittest.mock import patch, call
-
 from test.test_utils import (
     CALL_TIMEOUT_IN_SECONDS,
     send_initialize_request,
     send_notebook_did_open,
 )
+from unittest.mock import call, patch
 
 import pytest
-from pylsp.workspace import Notebook
 
 from pylsp import IS_WIN
 from pylsp.lsp import NotebookCellKind
+from pylsp.workspace import Notebook
 
 
 def wait_for_condition(condition, timeout=CALL_TIMEOUT_IN_SECONDS):
@@ -488,3 +487,46 @@ def test_notebook_definition(client_server_pair):
             },
         }
     ]
+
+
+@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows")
+def test_notebook_completion(client_server_pair):
+    """
+    Tests that completions work across cell boundaries for notebook document support
+    """
+    client, server = client_server_pair
+    send_initialize_request(client)
+
+    # Open notebook
+    with patch.object(server._endpoint, "notify") as mock_notify:
+        send_notebook_did_open(
+            client, ["answer_to_life_universe_everything = 42", "answer_"]
+        )
+        # wait for expected diagnostics messages
+        wait_for_condition(lambda: mock_notify.call_count >= 2)
+        assert len(server.workspace.documents) == 3
+        for uri in ["cell_1_uri", "cell_2_uri", "notebook_uri"]:
+            assert uri in server.workspace.documents
+
+    future = client._endpoint.request(
+        "textDocument/completion",
+        {
+            "textDocument": {
+                "uri": "cell_2_uri",
+            },
+            "position": {"line": 0, "character": 7},
+        },
+    )
+    result = future.result(CALL_TIMEOUT_IN_SECONDS)
+    assert result == {
+        "isIncomplete": False,
+        "items": [
+            {
+                "data": {"doc_uri": "cell_2_uri"},
+                "insertText": "answer_to_life_universe_everything",
+                "kind": 6,
+                "label": "answer_to_life_universe_everything",
+                "sortText": "aanswer_to_life_universe_everything",
+            },
+        ],
+    }
diff --git a/test/test_text_edit.py b/test/test_text_edit.py
index 63d0c90..2b49d24 100644
--- a/test/test_text_edit.py
+++ b/test/test_text_edit.py
@@ -1,8 +1,8 @@
 # Copyright 2017-2020 Palantir Technologies, Inc.
 # Copyright 2021- Python Language Server Contributors.
 
-from pylsp.text_edit import OverLappingTextEditException, apply_text_edits
 from pylsp import uris
+from pylsp.text_edit import OverLappingTextEditException, apply_text_edits
 
 DOC_URI = uris.from_fs_path(__file__)
 
diff --git a/test/test_uris.py b/test/test_uris.py
index 59fb209..f00973a 100644
--- a/test/test_uris.py
+++ b/test/test_uris.py
@@ -2,7 +2,9 @@
 # Copyright 2021- Python Language Server Contributors.
 
 from test import unix_only, windows_only
+
 import pytest
+
 from pylsp import uris
 
 
diff --git a/test/test_utils.py b/test/test_utils.py
index 8b518d7..6435efb 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -4,19 +4,18 @@
 import multiprocessing
 import os
 import sys
-from threading import Thread
 import time
+from threading import Thread
 from typing import Any, Dict, List
 from unittest import mock
 
-from flaky import flaky
 from docstring_to_markdown import UnknownFormatError
+from flaky import flaky
 
 from pylsp import _utils
 from pylsp.lsp import NotebookCellKind
 from pylsp.python_lsp import PythonLSPServer, start_io_lang_server
 
-
 CALL_TIMEOUT_IN_SECONDS = 30
 
 
diff --git a/test/test_workspace.py b/test/test_workspace.py
index 02ef1d2..c810bc5 100644
--- a/test/test_workspace.py
+++ b/test/test_workspace.py
@@ -3,8 +3,8 @@ import os
 import pathlib
 
 import pytest
-from pylsp import uris
 
+from pylsp import uris
 
 DOC_URI = uris.from_fs_path(__file__)
 NOTEBOOK_URI = uris.from_fs_path("notebook_uri")
@@ -238,7 +238,6 @@ def test_workspace_loads_pycodestyle_config(pylsp, tmpdir):
     # Test that project settings are loaded
     workspace2_dir = tmpdir.mkdir("NewTest456")
     cfg = workspace2_dir.join("pycodestyle.cfg")
-    # pylint: disable=implicit-str-concat
     cfg.write("[pycodestyle]\n" "max-line-length = 1000")
 
     workspace1 = {"uri": str(workspace1_dir)}
@@ -258,7 +257,6 @@ def test_workspace_loads_pycodestyle_config(pylsp, tmpdir):
     # Test switching to another workspace with different settings
     workspace3_dir = tmpdir.mkdir("NewTest789")
     cfg1 = workspace3_dir.join("pycodestyle.cfg")
-    # pylint: disable=implicit-str-concat
     cfg1.write("[pycodestyle]\n" "max-line-length = 20")
 
     workspace3 = {"uri": str(workspace3_dir)}