Repository: spyder-ide/spyder
Pull request: PR: Migrate completion plugin to the new API
<!--- Make sure to read the Contributing Guidelines:                   --->
<!--- https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md --->
<!--- and follow PEP 8, PEP 257 and Spyder's code style:               --->
<!--- https://github.com/spyder-ide/spyder/wiki/Dev:-Coding-Style      --->

## Description of Changes
This PR migrates the completion plugin (former CompletionManager) to the new API. As part of this PR, we also introduce a CompletionProvider interface, from which all completion providers inherit. The new CompletionPlugin will take all the providers (whom are registered via entrypoints), initialize them and manage them.

* [x] Wrote at least one-line docstrings (for any new functions)
* [x] Added unit test(s) covering the changes (if testable)
<!--- Remember that an image/animation is worth a thousand words! --->
* [ ] Included a screenshot or animation (if affecting the UI, see [Licecap](https://www.cockos.com/licecap/))


<!--- Explain what you've done and why --->




### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #12192


### Affirmation

By submitting this Pull Request or typing my (user)name below,
I affirm the [Developer Certificate of Origin](https://developercertificate.org)
with respect to all commits and content included in this PR,
and understand I am releasing the same under Spyder's MIT (Expat) license.

<!--- TYPE YOUR USER/NAME AFTER THE FOLLOWING: --->
I certify the above statement is true and correct: @andfoy

<!--- Thanks for your help making Spyder better for everyone! --->


Problem statement:
Move Completion plugin to use new API
- [ ] Refactor plugins to use the standard plugin API
- [ ] Add a extension manager for extensions
- [ ] Make sure tests pass
- [ ] Move to new API

Hints from issue discussion:

Commit message: Add status bar

Patch to review:
diff --git a/bootstrap.py b/bootstrap.py
index 2a3af4a2169..24d92895b57 100755
--- a/bootstrap.py
+++ b/bootstrap.py
@@ -24,6 +24,7 @@
 import argparse
 import os
 import os.path as osp
+import pkg_resources
 import shutil
 import subprocess
 import sys
@@ -178,6 +179,38 @@ def remove_pyls_installation():
 
 atexit.register(remove_pyls_installation)
 
+print("*. Declaring completion providers")
+# Register completion providers
+fallback = pkg_resources.EntryPoint.parse(
+    'fallback = spyder.plugins.completion.providers.fallback.provider:'
+    'FallbackProvider')
+
+snippets = pkg_resources.EntryPoint.parse(
+    'snippets = spyder.plugins.completion.providers.snippets.provider:'
+    'SnippetsProvider'
+)
+
+lsp = pkg_resources.EntryPoint.parse(
+    'lsp = spyder.plugins.completion.providers.languageserver.provider:'
+    'LanguageServerProvider'
+)
+
+# Create a fake Spyder distribution
+d = pkg_resources.Distribution(__file__)
+
+# Add the providers to the fake EntryPoint
+d._ep_map = {
+    'spyder.completions': {
+        'fallback': fallback,
+        'snippets': snippets,
+        'lsp': lsp
+    }
+}
+
+# Add the fake distribution to the global working_set
+pkg_resources.working_set.add(d, 'spyder')
+
+
 # Selecting the GUI toolkit: PyQt5 if installed
 if args.gui is None:
     try:
diff --git a/setup.py b/setup.py
index bd9a24e04d4..9a7582dccc5 100644
--- a/setup.py
+++ b/setup.py
@@ -268,18 +268,13 @@ def run(self):
 spyder_plugins_entry_points = [
     'appearance = spyder.plugins.appearance.plugin:Appearance',
     'breakpoints = spyder.plugins.breakpoints.plugin:Breakpoints',
-    ('code_completion = spyder.plugins.completion.manager.plugin:'
-     'CompletionManager'),
+    'completions = spyder.plugins.completion.plugin:CompletionPlugin',
     'editor = spyder.plugins.editor.plugin:Editor',
     'explorer = spyder.plugins.explorer.plugin:Explorer',
-    ('fallback_completion = spyder.plugins.completion.fallback.plugin:'
-     'FallbackPlugin'),
     'find_in_files = spyder.plugins.findinfiles.plugin:FindInFiles',
     'help = spyder.plugins.help.plugin:Help',
     'historylog = spyder.plugins.history.plugin:HistoryLog',
     'ipython_console = spyder.plugins.ipythonconsole.plugin:IPythonConsole',
-    ('kite_completion = spyder.plugins.completion.kite.plugin:'
-     'KiteCompletionPlugin'),
     'onlinehelp = spyder.plugins.onlinehelp.plugin:OnlineHelp',
     'outline_explorer = spyder.plugins.outlineexplorer.plugin:OutlineExplorer',
     'plots = spyder.plugins.plots.plugin:Plots',
@@ -287,9 +282,6 @@ def run(self):
     'profiler = spyder.plugins.profiler.plugin:Profiler',
     'project_explorer = spyder.plugins.projects.plugin:Projects',
     'pylint = spyder.plugins.pylint.plugin:Pylint',
-    ('lsp_completion = spyder.plugins.completion.languageserver.plugin:'
-     'LanguageServerPlugin'),
-    'python = spyder.plugins.python.plugin:Python',
     'statusbar = spyder.plugins.statusbar.plugin:StatusBar',
     ('variable_explorer = spyder.plugins.variableexplorer.plugin:'
      'VariableExplorer'),
@@ -297,6 +289,15 @@ def run(self):
      'WorkingDirectory'),
 ]
 
+spyder_completions_entry_points = [
+    ('fallback = spyder.plugins.completion.providers.fallback.provider:'
+     'FallbackProvider'),
+    ('snippets = spyder.plugins.completion.providers.snippets.provider:'
+     'SnippetsProvider'),
+    ('lsp = spyder.plugins.completion.providers.languageserver.provider:'
+     'LanguageServerProvider')
+]
+
 
 setup_args['install_requires'] = install_requires
 setup_args['extras_require'] = extras_require
@@ -305,6 +306,7 @@ def run(self):
             'spyder = spyder.app.start:main'
     ],
     'spyder.plugins': spyder_plugins_entry_points,
+    'spyder.completions': spyder_completions_entry_points
 }
 setup_args.pop('scripts', None)
 
diff --git a/spyder/api/plugins.py b/spyder/api/plugins.py
index e3b2b493849..a343fd36fb0 100644
--- a/spyder/api/plugins.py
+++ b/spyder/api/plugins.py
@@ -26,6 +26,7 @@
 import inspect
 import logging
 import os
+from typing import List, Union
 
 # Third party imports
 from qtpy.QtCore import QObject, Qt, Signal, Slot
@@ -136,6 +137,19 @@ def get_option(self, option, default=NoDefault, section=None):
         return super(BasePlugin, self)._get_option(option, default,
                                                    section=section)
 
+    def remove_option(self, option, section=None):
+        """
+        Remove an option from the Spyder configuration file.
+
+        Parameters
+        ----------
+        option: Union[str, Tuple[str, ...]]
+            A string or a Tuple of strings containing an option name to remove.
+        section: Optional[str]
+            Name of the section where the option belongs to.
+        """
+        return super(BasePlugin, self)._remove_option(option, section=section)
+
     def starting_long_process(self, message):
         """
         Show a message in main window's status bar and changes the
@@ -1053,15 +1067,39 @@ def set_conf_option(self, option, value, section=None):
                     'attribute!'
                 )
 
-            self._conf.set(section, str(option), value)
-            self.apply_conf({str(option)})
+            self._conf.set(section, option, value)
+            self.apply_conf({option}, False)
+
+    def remove_conf_option(self, option, section=None):
+        """
+        Delete an option in the Spyder configuration system.
+
+        Parameters
+        ----------
+        option: Union[str, Tuple[str, ...]]
+            Name of the option, either a string or a tuple of strings.
+        section: str
+            Section in the configuration system.
+        """
+        if self._conf is not None:
+            section = self.CONF_SECTION if section is None else section
+            if section is None:
+                raise SpyderAPIError(
+                    'A spyder plugin must define a `CONF_SECTION` class '
+                    'attribute!'
+                )
+
+            self._conf.remove_option(section, option)
+            self.apply_conf({option}, False)
 
-    def apply_conf(self, options_set):
+    def apply_conf(self, options_set, notify=True):
         """
         Apply `options_set` to this plugin's widget.
         """
         if self._conf is not None and options_set:
             container = self.get_container()
+            if notify:
+                self.after_configuration_update(list(options_set))
             # The container might not implement the SpyderWidgetMixin API
             # for example a completion client that only implements the
             # completion client interface without any options.
@@ -1358,6 +1396,20 @@ def after_container_creation(self):
         """
         pass
 
+    def after_configuration_update(self, options: List[Union[str, tuple]]):
+        """
+        Perform additional operations after updating the plugin configuration
+        values.
+
+        This can be implemented by plugins that do not have a container and
+        need to act on configuration updates.
+
+        Parameters
+        ----------
+        options: List[Union[str, tuple]]
+            A list that contains the options that were updated.
+        """
+        pass
 
 
 class SpyderDockablePlugin(SpyderPluginV2):
diff --git a/spyder/api/preferences.py b/spyder/api/preferences.py
index ca8abf25ab0..1cd6ea28ffd 100644
--- a/spyder/api/preferences.py
+++ b/spyder/api/preferences.py
@@ -8,14 +8,21 @@
 API to create an entry in Spyder Preferences associated to a given plugin.
 """
 
+# Standard library imports
+import types
+from typing import Tuple, Union, Set
+
 # Third party imports
 from qtpy.QtWidgets import QWidget
 
 # Local imports
-from spyder.plugins.preferences.api import SpyderConfigPage
+from spyder.plugins.preferences.api import SpyderConfigPage, BaseConfigTab
+
+
+OptionSet = Set[Union[str, Tuple[str, ...]]]
 
 
-class SpyderPreferencesTab(QWidget):
+class SpyderPreferencesTab(BaseConfigTab):
     """
     Widget that represents a tab on a preference page.
 
@@ -33,6 +40,16 @@ def __init__(self, parent: SpyderConfigPage):
         if self.TITLE is None or not isinstance(self.TITLE, str):
             raise ValueError("TITLE must be a str")
 
+    def apply_settings(self) -> OptionSet:
+        """
+        Hook called to manually apply settings that cannot be automatically
+        applied manually.
+
+        Reimplement this if the configuration tab has complex widgets that
+        cannot be created with any of the `self.create_*` calls.
+        """
+        return set({})
+
     def __getattr__(self, attr):
         this_class_dir = dir(self)
         if attr not in this_class_dir:
@@ -59,19 +76,32 @@ def __init__(self, plugin, parent):
         self.get_font = plugin.get_font
 
         if not self.APPLY_CONF_PAGE_SETTINGS:
-            try:
-                # New API
-                self.apply_settings = plugin.apply_conf
-                self.get_option = plugin.get_conf_option
-                self.set_option = plugin.set_conf_option
-            except AttributeError:
-                # Old API
-                self.apply_settings = plugin.apply_plugin_settings
-                self.get_option = plugin.get_option
-                self.set_option = plugin.set_option
+            self._patch_apply_settings(plugin)
 
         SpyderConfigPage.__init__(self, parent)
 
+    def _wrap_apply_settings(self, func):
+        def wrapper(self, options):
+            opts = self.previous_apply_settings()
+            func(options | opts)
+        return types.MethodType(wrapper, self)
+
+    def _patch_apply_settings(self, plugin):
+        self.previous_apply_settings = self.apply_settings
+        try:
+            # New API
+            self.apply_settings = self._wrap_apply_settings(plugin.apply_conf)
+            self.get_option = plugin.get_conf_option
+            self.set_option = plugin.set_conf_option
+            self.remove_option = plugin.remove_conf_option
+        except AttributeError:
+            # Old API
+            self.apply_settings = self._wrap_apply_settings(
+                plugin.apply_plugin_settings)
+            self.get_option = plugin.get_option
+            self.set_option = plugin.set_option
+            self.remove_option = plugin.remove_option
+
     def get_name(self):
         """
         Return plugin name to use in preferences page title, and
@@ -120,3 +150,16 @@ def setup_page(self):
         self.setLayout(layout)
         """
         raise NotImplementedError
+
+    def apply_settings(self) -> OptionSet:
+        """
+        Hook called to manually apply settings that cannot be automatically
+        applied manually.
+
+        Reimplement this if the configuration page has complex widgets that
+        cannot be created with any of the `self.create_*` calls.
+
+        This call should return a set containing the configuration options that
+        changed.
+        """
+        return set({})
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
index a87afc827a3..e5d3df9298a 100644
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -954,18 +954,18 @@ def create_edit_action(text, tr_text, icon):
                 menu_id=ApplicationMenus.Tools,
                 section=ToolsMenuSections.Tools,
                 before=winenv_action)
-        from spyder.plugins.completion.kite.utils.install import (
-            check_if_kite_installed)
-        is_kite_installed, kite_path = check_if_kite_installed()
-        if not is_kite_installed:
-            install_kite_action = create_action(
-                self, _("Install Kite completion engine"),
-                icon=get_icon('kite', adjust_for_interface=True),
-                triggered=self.show_kite_installation)
-            self.mainmenu.add_item_to_application_menu(
-                install_kite_action,
-                menu_id=ApplicationMenus.Tools,
-                section=ToolsMenuSections.Tools)
+        # from spyder.plugins.completion.kite.utils.install import (
+        #     check_if_kite_installed)
+        # is_kite_installed, kite_path = check_if_kite_installed()
+        # if not is_kite_installed:
+        #     install_kite_action = create_action(
+        #         self, _("Install Kite completion engine"),
+        #         icon=get_icon('kite', adjust_for_interface=True),
+        #         triggered=self.show_kite_installation)
+        #     self.mainmenu.add_item_to_application_menu(
+        #         install_kite_action,
+        #         menu_id=ApplicationMenus.Tools,
+        #         section=ToolsMenuSections.Tools)
         self.mainmenu.add_item_to_application_menu(
                 reset_spyder_action,
                 menu_id=ApplicationMenus.Tools)
@@ -1061,16 +1061,9 @@ def create_edit_action(text, tr_text, icon):
         self.register_plugin(self.maininterpreter)
 
         # Code completion client initialization
-        self.set_splash(_("Starting code completion manager..."))
-        from spyder.plugins.completion.manager.plugin import CompletionManager
-        self.completions = CompletionManager(self)
-        self.completions.start()
-
-        self.preferences.register_plugin_preferences(self.completions)
-
-        for completion_plugin in self.completions.clients.values():
-            completion_plugin = completion_plugin['plugin']
-            self.preferences.register_plugin_preferences(completion_plugin)
+        from spyder.plugins.completion.plugin import CompletionPlugin
+        self.completions = CompletionPlugin(self, configuration=CONF)
+        self.register_plugin(self.completions)
 
         # Outline explorer widget
         if CONF.get('outline_explorer', 'enable'):
@@ -1496,7 +1489,7 @@ def post_visible_setup(self):
                 self.editor.setup_open_files(close_previous_files=False)
 
         # Connect Editor to Kite completions plugin status
-        self.editor.kite_completions_file_status()
+        # self.editor.kite_completions_file_status()
 
         # Connect Editor debug action with Console
         self.ipyconsole.sig_pdb_state.connect(self.editor.update_pdb_state)
@@ -2548,8 +2541,8 @@ def closing(self, cancelable=False):
         if CONF.get('main', 'single_instance') and self.open_files_server:
             self.open_files_server.close()
 
-        if not self.completions.closing_plugin(cancelable):
-            return False
+        # if not self.completions.closing_plugin(cancelable):
+        #     return False
 
         # Internal plugins
         for plugin in (self.widgetlist + self.thirdparty_plugins):
@@ -2587,7 +2580,7 @@ def closing(self, cancelable=False):
         prefix = 'window' + '/'
         self.save_current_window_settings(prefix)
 
-        self.completions.shutdown()
+        # self.completions.shutdown()
 
         self.already_closed = True
         return True
diff --git a/spyder/config/main.py b/spyder/config/main.py
index 101fe25e4df..20036f8c4ef 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -223,7 +223,6 @@
               'automatic_completions': True,
               'automatic_completions_after_chars': 3,
               'automatic_completions_after_ms': 300,
-              'completions_wait_for_ms': 200,
               'completions_hint': True,
               'completions_hint_after_ms': 500,
               'underline_errors': False,
@@ -313,6 +312,16 @@
              {
               'enable': True,
               }),
+            ('completions',
+             {
+               'enable': True,
+               'enable_code_snippets': True,
+               'completions_wait_for_ms': 200,
+               'enabled_providers': {},
+               'provider_configuration': {},
+               'request_priorities': {}
+             }
+            ),
             ('profiler',
              {
               'enable': True,
diff --git a/spyder/config/manager.py b/spyder/config/manager.py
index 8c71c72d6a1..04cc0f9ec74 100644
--- a/spyder/config/manager.py
+++ b/spyder/config/manager.py
@@ -15,7 +15,7 @@
 # Local imports
 from spyder.config.base import _, get_conf_paths, get_conf_path, get_home_dir
 from spyder.config.main import CONF_VERSION, DEFAULTS, NAME_MAP
-from spyder.config.user import UserConfig, MultiUserConfig, NoDefault
+from spyder.config.user import UserConfig, MultiUserConfig, NoDefault, cp
 from spyder.utils.programs import check_version
 
 
@@ -225,7 +225,25 @@ def get(self, section, option, default=NoDefault):
         If section is None, the `option` is requested from default section.
         """
         config = self.get_active_conf(section)
-        return config.get(section=section, option=option, default=default)
+        if isinstance(option, (list, tuple)):
+            base_option = option[0]
+            intermediate_options = option[1:-1]
+            last_option = option[-1]
+
+            base_conf = config.get(
+                section=section, option=base_option, default={})
+            next_ptr = base_conf
+            for opt in intermediate_options:
+                next_ptr = next_ptr.get(opt, {})
+
+            value = next_ptr.get(last_option, None)
+            if value is None:
+                value = default
+                if default is NoDefault:
+                    raise cp.NoOptionError(option, section)
+        else:
+            value = config.get(section=section, option=option, default=default)
+        return value
 
     def set(self, section, option, value, verbose=False, save=True):
         """
@@ -233,6 +251,23 @@ def set(self, section, option, value, verbose=False, save=True):
 
         If section is None, the `option` is added to the default section.
         """
+        import inspect
+        if isinstance(option, (list, tuple)):
+            base_option = option[0]
+            intermediate_options = option[1:-1]
+            last_option = option[-1]
+
+            base_conf = self.get(section, base_option, {})
+            conf_ptr = base_conf
+            for opt in intermediate_options:
+                next_ptr = conf_ptr.get(opt, {})
+                conf_ptr[opt] = next_ptr
+                conf_ptr = next_ptr
+
+            conf_ptr[last_option] = value
+            value = base_conf
+            option = base_option
+
         config = self.get_active_conf(section)
         config.set(section=section, option=option, value=value,
                    verbose=verbose, save=save)
@@ -244,6 +279,18 @@ def get_default(self, section, option):
         This is useful for type checking in `get` method.
         """
         config = self.get_active_conf(section)
+        if isinstance(option, (list, tuple)):
+            base_option = option[0]
+            intermediate_options = option[1:-1]
+            last_option = option[-1]
+
+            base_default = config.get_default(section, base_option)
+            conf_ptr = base_default
+            for opt in intermediate_options:
+                conf_ptr = conf_ptr[opt]
+
+            return conf_ptr[last_option]
+
         return config.get_default(section, option)
 
     def remove_section(self, section):
@@ -254,7 +301,19 @@ def remove_section(self, section):
     def remove_option(self, section, option):
         """Remove `option` from `section`."""
         config = self.get_active_conf(section)
-        config.remove_option(section, option)
+        if isinstance(option, (list, tuple)):
+            base_option = option[0]
+            intermediate_options = option[1:-1]
+            last_option = option[-1]
+
+            base_conf = self.get(section, base_option)
+            conf_ptr = base_conf
+            for opt in intermediate_options:
+                conf_ptr = conf_ptr[opt]
+            conf_ptr.pop(last_option)
+            self.set(section, base_option)
+        else:
+            config.remove_option(section, option)
 
     def reset_to_defaults(self, section=None):
         """Reset config to Default values."""
diff --git a/spyder/plugins/base.py b/spyder/plugins/base.py
index d503bd74244..c6e4a5e8157 100644
--- a/spyder/plugins/base.py
+++ b/spyder/plugins/base.py
@@ -68,6 +68,11 @@ def _get_option(self, option, default=NoDefault, section=None):
         section = self.CONF_SECTION if section is None else section
         return CONF.get(section, option, default)
 
+    def _remove_option(self, option, section=None):
+        """Remove option from spyder.ini."""
+        section = self.CONF_SECTION if section is None else section
+        CONF.remove_option(section, option)
+
     def _show_status_message(self, message, timeout=0):
         """Show message in main window's status bar."""
         self.main.statusBar().showMessage(message, timeout)
diff --git a/spyder/plugins/completion/fallback/__init__.py b/spyder/plugins/completion/__init__.py
similarity index 100%
rename from spyder/plugins/completion/fallback/__init__.py
rename to spyder/plugins/completion/__init__.py
diff --git a/spyder/plugins/completion/manager/api.py b/spyder/plugins/completion/api.py
similarity index 67%
rename from spyder/plugins/completion/manager/api.py
rename to spyder/plugins/completion/api.py
index 996b1512eee..d8f55679aff 100644
--- a/spyder/plugins/completion/manager/api.py
+++ b/spyder/plugins/completion/api.py
@@ -5,197 +5,97 @@
 # (see spyder/__init__.py for details)
 
 """
-LSP client, code introspection and linting utilities.
-"""
-
-from qtpy.QtCore import QObject, Signal
-from spyder.api.plugins import SpyderPlugin
-
-
-class SpyderCompletionPlugin(QObject, SpyderPlugin):
-    """
-    Spyder plugin API for completion clients.
-
-    All completion clients must implement this interface in order to interact
-    with Spyder CodeEditor and Projects manager.
-    """
-
-    # Use this signal to send a response back to the completion manager
-    # str: Completion client name
-    # int: Request sequence identifier
-    # dict: Response dictionary
-    sig_response_ready = Signal(str, int, dict)
-
-    # Use this signal to indicate that the plugin is ready
-    sig_plugin_ready = Signal(str)
-
-    # ---------------------------- ATTRIBUTES ---------------------------------
-
-    # Name of the completion service
-    # Status: Required
-    COMPLETION_CLIENT_NAME = None
-
-    def __init__(self, parent):
-        QObject.__init__(self, parent)
-        SpyderPlugin.__init__(self, parent)
-        self.main = parent
-
-    def register_file(self, language, filename, codeeditor):
-        """
-        Register file to perform completions.
-        If a language client is not available for a given file, then this
-        method should keep a queue, such that files can be initialized once
-        a server is available.
-
-        Parameters
-        ----------
-        language: str
-            Programming language of the given file
-        filename: str
-            Filename to register
-        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
-            Codeeditor to send the client configurations
-        """
-        pass
-
-    def send_request(self, language, req_type, req, req_id):
-        """
-        Process completion/introspection request from Spyder.
+General Spyder completion API constants and enumerations.
 
-        Parameters
-        ----------
-        language: str
-            Programming language for the incoming request
-        req_type: str
-            Type of request, one of
-            :class:`spyder.plugins.completion.CompletionTypes`
-        req: dict
-            Request body
-            {
-                'filename': str,
-                **kwargs: request-specific parameters
-            }
-        req_id: int
-            Request identifier for response
-        """
-        pass
-
-    def send_notification(self, language, notification_type, notification):
-        """
-        Send notification to completion server based on Spyder changes.
-
-        Parameters
-        ----------
-        language: str
-            Programming language for the incoming request
-        notification_type: str
-            Type of request, one of
-            :class:`spyder.plugins.completion.CompletionTypes`
-        notification: dict
-            Request body
-            {
-                'filename': str,
-                **kwargs: request-specific parameters
-            }
-        """
-        pass
-
-    def send_response(self, response, resp_id):
-        """
-        Send response for server request.
-
-        Parameters
-        ----------
-        response: dict
-            Response body for server
-            {
-                **kwargs: response-specific keys
-            }
-        resp_id: int
-            Request identifier for response
-        """
-        pass
-
-    def broadcast_notification(self, notification_type, notification):
-        """
-        Send a broadcast notification across all programming languages.
-
-        Parameters
-        ----------
-        req_type: str
-            Type of request, one of
-            :class:`spyder.plugins.completion.CompletionTypes`
-        req: dict
-            Request body
-            {
-                **kwargs: notification-specific parameters
-            }
-        req_id: int
-            Request identifier for response, None if notification
-        """
-        pass
+The constants and enums presented on this file correspond to a superset of
+those used by the Language Server Protocol (LSP), available at:
+https://microsoft.github.io/language-server-protocol/specifications/specification-current/
+"""
 
-    def update_configuration(self):
-        """Handle completion option configuration updates."""
-        pass
+# Standard library imports
+from typing import Any, Optional, Tuple, Union
 
-    def project_path_update(self, project_path, update_kind):
-        """
-        Handle project path updates on Spyder.
+# Third party imports
+from qtpy.QtCore import Signal, QObject, Slot
 
-        Parameters
-        ----------
-        project_path: str
-            Path to the project folder modified
-        update_kind: str
-            Path update kind, one of
-            :class:`spyder.plugins.completion.WorkspaceUpdateKind`
-        """
-        pass
 
-    def start_client(self, language):
-        """
-        Start completions/introspection services for a given language.
+# Supported LSP programming languages
+SUPPORTED_LANGUAGES = [
+    'Bash', 'C#', 'Cpp', 'CSS/LESS/SASS', 'Go', 'GraphQL', 'Groovy', 'Elixir',
+    'Erlang', 'Fortran', 'Haxe', 'HTML', 'Java', 'JavaScript', 'JSON',
+    'Julia', 'Kotlin', 'OCaml', 'PHP', 'R', 'Rust', 'Scala', 'Swift',
+    'TypeScript'
+]
 
-        Parameters
-        ----------
-        language: str
-            Programming language to start analyzing
+# ------------------ WORKSPACE SYMBOLS CONSTANTS --------------------
 
-        Returns
-        -------
-        bool
-            True if language client could be started, otherwise False.
-        """
-        return False
 
-    def stop_client(self, language):
-        """
-        Stop completions/introspection services for a given language.
-
-        Parameters
-        ----------
-        language: str
-            Programming language to stop analyzing
-        """
-        pass
+class SymbolKind:
+    """LSP workspace symbol constants."""
+    FILE = 1
+    MODULE = 2
+    NAMESPACE = 3
+    PACKAGE = 4
+    CLASS = 5
+    METHOD = 6
+    PROPERTY = 7
+    FIELD = 8
+    CONSTRUCTOR = 9
+    ENUM = 10
+    INTERFACE = 11
+    FUNCTION = 12
+    VARIABLE = 13
+    CONSTANT = 14
+    STRING = 15
+    NUMBER = 16
+    BOOLEAN = 17
+    ARRAY = 18
+    OBJECT = 19
+    KEY = 20
+    NULL = 21
+    ENUM_MEMBER = 22
+    STRUCT = 23
+    EVENT = 24
+    OPERATOR = 25
+    TYPE_PARAMETER = 26
 
-    def start(self):
-        """Start completion plugin."""
-        self.sig_plugin_ready.emit(self.COMPLETION_CLIENT_NAME)
+    # Additional symbol constants (non-standard)
+    BLOCK_COMMENT = 224
+    CELL = 225
 
-    def shutdown(self):
-        """Stop completion plugin."""
-        pass
 
+# Mapping between symbol enum and icons
+SYMBOL_KIND_ICON = {
+    SymbolKind.FILE: 'file',
+    SymbolKind.MODULE: 'module',
+    SymbolKind.NAMESPACE: 'namespace',
+    SymbolKind.PACKAGE: 'package',
+    SymbolKind.CLASS: 'class',
+    SymbolKind.METHOD: 'method',
+    SymbolKind.PROPERTY: 'property',
+    SymbolKind.FIELD: 'field',
+    SymbolKind.CONSTRUCTOR: 'constructor',
+    SymbolKind.ENUM: 'enum',
+    SymbolKind.INTERFACE: 'interface',
+    SymbolKind.FUNCTION: 'function',
+    SymbolKind.VARIABLE: 'variable',
+    SymbolKind.CONSTANT: 'constant',
+    SymbolKind.STRING: 'string',
+    SymbolKind.NUMBER: 'number',
+    SymbolKind.BOOLEAN: 'boolean',
+    SymbolKind.ARRAY: 'array',
+    SymbolKind.OBJECT: 'object',
+    SymbolKind.KEY: 'key',
+    SymbolKind.NULL: 'null',
+    SymbolKind.ENUM_MEMBER: 'enum_member',
+    SymbolKind.STRUCT: 'struct',
+    SymbolKind.EVENT: 'event',
+    SymbolKind.OPERATOR: 'operator',
+    SymbolKind.TYPE_PARAMETER: 'type_parameter',
+    SymbolKind.BLOCK_COMMENT: 'blockcomment',
+    SymbolKind.CELL: 'cell'
+}
 
-# Supported LSP programming languages
-LSP_LANGUAGES = [
-    'Bash', 'C#', 'Cpp', 'CSS/LESS/SASS', 'Go', 'GraphQL', 'Groovy', 'Elixir',
-    'Erlang', 'Fortran', 'Haxe', 'HTML', 'Java', 'JavaScript', 'JSON',
-    'Julia', 'Kotlin', 'OCaml', 'PHP', 'R', 'Rust', 'Scala', 'Swift',
-    'TypeScript', 'STIL'
-]
 
 # -------------------- WORKSPACE CONFIGURATION CONSTANTS ----------------------
 
@@ -230,36 +130,6 @@ class FailureHandlingKind:
     TEXT_ONLY_TRANSACTIONAL = 'textOnlyTransactional'
 
 
-class SymbolKind:
-    """LSP workspace symbol constants."""
-    FILE = 1
-    MODULE = 2
-    NAMESPACE = 3
-    PACKAGE = 4
-    CLASS = 5
-    METHOD = 6
-    PROPERTY = 7
-    FIELD = 8
-    CONSTRUCTOR = 9
-    ENUM = 10
-    INTERFACE = 11
-    FUNCTION = 12
-    VARIABLE = 13
-    CONSTANT = 14
-    STRING = 15
-    NUMBER = 16
-    BOOLEAN = 17
-    ARRAY = 18
-    OBJECT = 19
-    KEY = 20
-    NULL = 21
-    ENUM_MEMBER = 22
-    STRUCT = 23
-    EVENT = 24
-    OPERATOR = 25
-    TYPE_PARAMETER = 26
-
-
 # -------------------- CLIENT CONFIGURATION SETTINGS --------------------------
 
 # WorkspaceClientCapabilities define capabilities the
@@ -660,7 +530,7 @@ class LSPEventTypes:
     CODE_LENS = 'codeLens'
 
 
-class LSPRequestTypes:
+class CompletionRequestTypes:
     """Language Server Protocol request/response types."""
     # General requests
     INITIALIZE = 'initialize'
@@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
+
+    def __init__(self, parent, config):
+        """
+        Main completion provider constructor.
+
+        Parameters
+        ----------
+        parent: spyder.plugins.completion.plugin.CompletionPlugin
+            Instance of the completion plugin that manages this provider
+        config: dict
+            Current provider configuration values, whose keys correspond to
+            the ones defined on `CONF_DEFAULTS` and the values correspond to
+            the current values according to the Spyder configuration.
+        """
+        QObject.__init__(self, parent)
+        self.main = parent
+        self.config = config
+
+    def get_name(self) -> str:
+        """Return a human readable name of the completion provider."""
+        return ''
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        pass
+
+    def send_request(
+            self, language: str, req_type: str, req: dict, req_id: int):
+        """
+        Process completion/introspection request from Spyder.
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        pass
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send notification to completion server based on Spyder changes.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        """
+        pass
+
+    def broadcast_notification(
+            self, notification_type: str, notification: dict):
+        """
+        Send a broadcast notification across all programming languages.
+
+        Parameters
+        ----------
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.CompletionTypes`
+        req: dict
+            Request body
+            {
+                **kwargs: notification-specific parameters
+            }
+        req_id: int
+            Request identifier for response, None if notification
+        """
+        pass
+
+    def send_response(self, response: dict, resp_id: int):
+        """
+        Send response for server request.
+
+        Parameters
+        ----------
+        response: dict
+            Response body for server
+            {
+                **kwargs: response-specific keys
+            }
+        resp_id: int
+            Request identifier for response
+        """
+        pass
+
+    def update_configuration(self, config: dict):
+        """
+        Handle completion option configuration updates.
+
+        Parameters
+        ----------
+        conf: dict
+            Dictionary containing the new provider configuration
+        """
+        pass
+
+    def project_path_update(self, project_path: str, update_kind: str,
+                            instance: Any):
+        """
+        Handle project path updates on Spyder.
+
+        Parameters
+        ----------
+        project_path: str
+            Path to the project folder modified
+        update_kind: str
+            Path update kind, one of
+            :class:`spyder.plugins.completion.api.WorkspaceUpdateKind`
+        instance: object
+            Reference to :class:`spyder.plugins.projects.plugin.Projects`
+        """
+        pass
+
+    @Slot(object, object)
+    def python_path_update(self, previous_path, new_path):
+        """
+        Handle Python path updates on Spyder.
+
+        Parameters
+        ----------
+        previous_path: Dict
+            Dictionary containing the previous Python path values
+        new_path: Dict
+            Dictionary containing the current Python path values.
+        """
+        pass
+
+    @Slot()
+    def main_interpreter_changed(self):
+        """Handle changes on the main Python interpreter of Spyder."""
+        pass
+
+    def editor_focus_changed(
+            self, filename: str, language: str):
+        """
+        Handle focus changes across tabs and split editors.
+
+        Parameters
+        ----------
+        filename: str
+            Path to the file currently focused on the editor.
+        language: str
+            Name of the programming language of the currently focused file.
+        """
+        pass
+
+    def start_provider(self, language: str) -> bool:
+        """
+        Start completions/introspection services for a given language.
+
+        Parameters
+        ----------
+        language: str
+            Programming language to start analyzing
+
+        Returns
+        -------
+        bool
+            True if language client could be started, otherwise False.
+        """
+        return False
+
+    def stop_provider(self, language: str):
+        """
+        Stop completions/introspection services for a given language.
+
+        Parameters
+        ----------
+        language: str
+            Programming language to stop analyzing
+        """
+        pass
+
+    def start(self):
+        """Start completion plugin."""
+        self.sig_provider_ready.emit(self.COMPLETION_CLIENT_NAME)
+
+    def shutdown(self):
+        """Stop completion plugin."""
+        pass
+
+    def can_close(self) -> bool:
+        """Establish if the current completion provider can be stopped."""
+        return True
+
+    def get_option(self,
+                   option_name: Union[str, Tuple[str, ...]],
+                   default: Any = None,
+                   section: Optional[str] = None) -> Any:
+        """
+        Retrieve an option value from the provider settings dictionary or
+        from the global Spyder configurations.
+
+        Parameters
+        ----------
+        option_name: str
+            Option name to lookup for in the provider settings
+            dictionary/global Spyder configurations.
+        default: Any
+            Default value to return if `option_name` was not found.
+        section: Optional[str]
+            If None, then the option is retrieved from the local provider
+            configurations. Otherwise, lookup on the global Spyder
+            configurations.
+
+        Returns
+        -------
+        Any
+            Either the default value if `option_name` was not found on the
+            settings or the actual value stored.
+        """
+        if section is None:
+            section = 'completions'
+            if isinstance(option_name, tuple):
+                option_name = (
+                    'provider_configuration',
+                    self.COMPLETION_CLIENT_NAME,
+                    'values',
+                    *option_name
+                )
+            else:
+                option_name = (
+                    'provider_configuration',
+                    self.COMPLETION_CLIENT_NAME,
+                    'values',
+                    option_name
+                )
+        return self.main.get_global_option(
+            option_name, section, default)
+
+    def set_option(self,
+                   option_name: Union[str, Tuple[str, ...]],
+                   value: Any,
+                   section: Optional[str] = None):
+        """
+        Set an option in the provider configuration settings dictionary or
+        in the global Spyder configurations.
+
+        Parameters
+        ----------
+        option_name: str
+            Option name to lookup for in the provider settings
+            dictionary/global Spyder configurations.
+        value: Any
+            Value to set in the configuration entry.
+        section: Optional[str]
+            If None, then the option is retrieved from the local provider
+            configurations. Otherwise, lookup on the global Spyder
+            configurations.
+        """
+        if section is None:
+            section = 'completions'
+            if isinstance(option_name, tuple):
+                option_name = (
+                    'provider_configuration',
+                    self.COMPLETION_CLIENT_NAME,
+                    'values',
+                    *option_name
+                )
+            else:
+                option_name = (
+                    'provider_configuration',
+                    self.COMPLETION_CLIENT_NAME,
+                    'values',
+                    option_name
+                )
+        self.main.set_conf_option(option_name, value, section)
diff --git a/spyder/plugins/completion/confpage.py b/spyder/plugins/completion/confpage.py
new file mode 100644
index 00000000000..c3b8e1a5aa2
--- /dev/null
+++ b/spyder/plugins/completion/confpage.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Spyder completion plugin configuration page."""
+
+# Third party imports
+from qtpy.QtWidgets import QGroupBox, QVBoxLayout, QCheckBox, QGridLayout
+
+# Local imports
+from spyder.config.base import _
+from spyder.utils import icon_manager as ima
+from spyder.api.preferences import PluginConfigPage
+
+
+class CompletionConfigPage(PluginConfigPage):
+    def __init__(self, plugin, parent, providers=[]):
+        super().__init__(plugin, parent)
+        self.providers = providers
+
+    def setup_page(self):
+        newcb = self.create_checkbox
+
+        # -------------------- Plugin state group -----------------------------
+        plugin_state_group = QGroupBox(_('Plugin status'))
+        plugin_state_layout = QVBoxLayout()
+        self.completion_box = newcb(_("Enable Completion and linting plugin"),
+                                    'enable')
+        plugin_state_layout.addWidget(self.completion_box)
+        plugin_state_group.setLayout(plugin_state_layout)
+
+        self.completion_box.toggled.connect(self.enable_disable_plugin)
+
+        # ------------------- Providers state group ---------------------------
+        self.provider_checkboxes = []
+        providers_layout = QGridLayout()
+        self.providers_group = QGroupBox(_("Completion providers"))
+        for i, (provider_key, provider_name) in enumerate(self.providers):
+            cb = newcb(_('Enable {0} provider').format(provider_name),
+                       ('enabled_providers', provider_key), default=True)
+            providers_layout.addWidget(cb, i, 0)
+            self.provider_checkboxes.append(cb)
+
+        self.providers_group.setLayout(providers_layout)
+
+        completions_wait_for_ms = self.create_spinbox(
+            _("Time to wait for all providers to return (ms):"), None,
+            'completions_wait_for_ms', min_=0, max_=10000, step=10,
+            tip=_("Beyond this timeout, "
+                "the first available provider will be returned"))
+        completion_hint_box = newcb(
+            _("Show completion details"),
+            'completions_hint',
+            section='editor')
+        automatic_completion_box = newcb(
+            _("Show completions on the fly"),
+            'automatic_completions',
+            section='editor')
+        completions_after_characters = self.create_spinbox(
+            _("Show automatic completions after characters entered:"), None,
+            'automatic_completions_after_chars', min_=1, step=1,
+            tip=_("Default is 3"), section='editor')
+        code_snippets_box = newcb(
+            _("Enable code snippets in the editor"), 'enable_code_snippets')
+
+        self.advanced_group = QGroupBox(_('Advanced settings'))
+        advanced_layout = QGridLayout()
+        advanced_layout.addWidget(completion_hint_box, 0, 0)
+        advanced_layout.addWidget(code_snippets_box, 1, 0)
+        advanced_layout.addWidget(automatic_completion_box, 2, 0)
+        advanced_layout.addWidget(completions_after_characters.plabel, 3, 0)
+        advanced_layout.addWidget(completions_after_characters.spinbox, 3, 1)
+        advanced_layout.addWidget(completions_wait_for_ms.plabel, 4, 0)
+        advanced_layout.addWidget(completions_wait_for_ms.spinbox, 4, 1)
+        advanced_layout.setColumnStretch(2, 6)
+        self.advanced_group.setLayout(advanced_layout)
+
+        def disable_completion_after_characters(state):
+            completions_after_characters.plabel.setEnabled(state)
+            completions_after_characters.spinbox.setEnabled(state)
+
+        automatic_completion_box.toggled.connect(
+            disable_completion_after_characters)
+
+        layout = QVBoxLayout()
+        layout.addWidget(plugin_state_group)
+        layout.addWidget(self.providers_group)
+        layout.addWidget(self.advanced_group)
+        layout.addStretch(1)
+        self.setLayout(layout)
+
+    def enable_disable_plugin(self, state):
+        self.providers_group.setEnabled(state)
+        self.advanced_group.setEnabled(state)
+
+        if self.tabs is not None:
+            num_tabs = self.tabs.count()
+            index = 1
+            while index < num_tabs:
+                tab_widget = self.tabs.widget(index)
+                tab_widget.setEnabled(state)
+                index += 1
diff --git a/spyder/plugins/completion/container.py b/spyder/plugins/completion/container.py
new file mode 100644
index 00000000000..4decc5f91ff
--- /dev/null
+++ b/spyder/plugins/completion/container.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Spyder completion container."""
+
+# Standard library imports
+
+# Third-party imports
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.widgets import PluginMainContainer
+
+
+class CompletionContainer(PluginMainContainer):
+    """Stateless class used to store graphical widgets."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.statusbars = {}
+
+    def setup(self, options=None):
+        pass
+
+    def update_actions(self):
+        pass
+
+    def on_option_update(self, _option, _value):
+        pass
+
+    def show_widget(self, Widget):
+        widget = Widget(self)
+
+        if isinstance(widget, QMessageBox):
+            if hasattr(widget, 'sig_restart_spyder'):
+                widget.sig_restart_spyder.connect(self.sig_restart_requested)
+            widget.exec_()
+
+    def register_statusbars(self, statusbar_map):
+        for status_key in statusbar_map:
+            StatusBar = statusbar_map[status_key]
+            self.statusbars[status_key] = StatusBar(self)
+
+    def all_statusbars(self):
+        return [self.statusbars[k] for k in self.statusbars]
+
+    def statusbar_rpc(self, status_key, method, args, kwargs):
+        statusbar = self.statusbars[status_key]
+        call = getattr(statusbar, method)
+        call(*args, **kwargs)
diff --git a/spyder/plugins/completion/manager/decorators.py b/spyder/plugins/completion/decorators.py
similarity index 100%
rename from spyder/plugins/completion/manager/decorators.py
rename to spyder/plugins/completion/decorators.py
diff --git a/spyder/plugins/completion/manager/__init__.py b/spyder/plugins/completion/manager/__init__.py
deleted file mode 100644
index 3a077f9cb49..00000000000
--- a/spyder/plugins/completion/manager/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""
-Completion manager plugin and completion plugins for the Editor.
-"""
diff --git a/spyder/plugins/completion/manager/plugin.py b/spyder/plugins/completion/manager/plugin.py
deleted file mode 100644
index 246625d1d5f..00000000000
--- a/spyder/plugins/completion/manager/plugin.py
+++ /dev/null
@@ -1,404 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""
-Backend plugin to manage multiple code completion and introspection clients.
-"""
-
-# Standard library imports
-from collections import defaultdict
-import logging
-
-# Third-party imports
-from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot
-from qtpy.QtWidgets import QMessageBox
-
-# Local imports
-from spyder.config.base import _
-from spyder.plugins.completion.languageserver.plugin import (
-    LanguageServerPlugin)
-from spyder.plugins.completion.kite.plugin import KiteCompletionPlugin
-from spyder.plugins.completion.fallback.plugin import FallbackPlugin
-from spyder.plugins.completion.manager.api import (LSPRequestTypes,
-                                                   SpyderCompletionPlugin)
-from spyder.plugins.completion.snippets.plugin import SnippetsPlugin
-
-logger = logging.getLogger(__name__)
-
-ALL_COMPLETION_PLUGINS = {
-    p.COMPLETION_CLIENT_NAME: p for p in (
-        LanguageServerPlugin,
-        FallbackPlugin,
-        KiteCompletionPlugin,
-        SnippetsPlugin
-    )
-}
-
-class CompletionManager(SpyderCompletionPlugin):
-    STOPPED = 'stopped'
-    RUNNING = 'running'
-
-    BASE_PLUGINS = ALL_COMPLETION_PLUGINS
-
-    WAIT_FOR_SOURCE = defaultdict(
-        lambda: {LanguageServerPlugin.COMPLETION_CLIENT_NAME},
-        {
-            LSPRequestTypes.DOCUMENT_COMPLETION: {
-                KiteCompletionPlugin.COMPLETION_CLIENT_NAME,
-                LanguageServerPlugin.COMPLETION_CLIENT_NAME,
-            },
-            LSPRequestTypes.DOCUMENT_SIGNATURE: {
-                KiteCompletionPlugin.COMPLETION_CLIENT_NAME,
-                LanguageServerPlugin.COMPLETION_CLIENT_NAME,
-            },
-            LSPRequestTypes.DOCUMENT_HOVER: {
-                KiteCompletionPlugin.COMPLETION_CLIENT_NAME,
-                LanguageServerPlugin.COMPLETION_CLIENT_NAME,
-            },
-        })
-
-    SOURCE_PRIORITY = defaultdict(
-        lambda: (
-            LanguageServerPlugin.COMPLETION_CLIENT_NAME,
-            KiteCompletionPlugin.COMPLETION_CLIENT_NAME,
-            SnippetsPlugin.COMPLETION_CLIENT_NAME,
-            FallbackPlugin.COMPLETION_CLIENT_NAME,
-        ), {
-            LSPRequestTypes.DOCUMENT_COMPLETION: (
-                KiteCompletionPlugin.COMPLETION_CLIENT_NAME,
-                LanguageServerPlugin.COMPLETION_CLIENT_NAME,
-                SnippetsPlugin.COMPLETION_CLIENT_NAME,
-                FallbackPlugin.COMPLETION_CLIENT_NAME,
-            ),
-        })
-
-    SKIP_INTERMEDIATE_REQUESTS = {
-        LSPRequestTypes.DOCUMENT_COMPLETION
-    }
-
-    def __init__(self, parent, plugins=ALL_COMPLETION_PLUGINS):
-        SpyderCompletionPlugin.__init__(self, parent)
-        self.clients = {}
-        self.requests = {}
-        self.language_status = {}
-        self.started = False
-        self.req_id = 0
-        self.collection_mutex = QMutex(QMutex.Recursive)
-        self.wait_for_ms = self.get_option('completions_wait_for_ms',
-                                           section='editor')
-
-        for plugin in plugins:
-            if plugin in self.BASE_PLUGINS:
-                Plugin = self.BASE_PLUGINS[plugin]
-                plugin_client = Plugin(self.main)
-                self.register_completion_plugin(plugin_client)
-
-    def register_completion_plugin(self, plugin):
-        logger.debug("Completion plugin: Registering {0}".format(
-            plugin.COMPLETION_CLIENT_NAME))
-        plugin_name = plugin.COMPLETION_CLIENT_NAME
-        self.clients[plugin_name] = {
-            'plugin': plugin,
-            'status': self.STOPPED
-        }
-        plugin.sig_response_ready.connect(self.receive_response)
-        plugin.sig_plugin_ready.connect(self.client_available)
-        for language in self.language_status:
-            server_status = self.language_status[language]
-            server_status[plugin_name] = False
-
-    @Slot(str, int, dict)
-    def receive_response(self, completion_source, req_id, resp):
-        logger.debug("Completion plugin: Request {0} Got response "
-                     "from {1}".format(req_id, completion_source))
-
-        if req_id not in self.requests:
-            return
-
-        with QMutexLocker(self.collection_mutex):
-            request_responses = self.requests[req_id]
-            request_responses['sources'][completion_source] = resp
-            self.howto_send_to_codeeditor(req_id)
-
-    @Slot(int)
-    def receive_timeout(self, req_id):
-        # On timeout, collect all completions and return to the user
-        if req_id not in self.requests:
-            return
-
-        logger.debug("Completion plugin: Request {} timed out".format(req_id))
-
-        with QMutexLocker(self.collection_mutex):
-            request_responses = self.requests[req_id]
-            request_responses['timed_out'] = True
-            self.howto_send_to_codeeditor(req_id)
-
-    def howto_send_to_codeeditor(self, req_id):
-        """
-        Decide how to send the responses corresponding to req_id to
-        the CodeEditor instance that requested them.
-        """
-        if req_id not in self.requests:
-            return
-        request_responses = self.requests[req_id]
-        language = request_responses['language']
-
-        # Skip the waiting logic below when fallback is the only
-        # client that works for language
-        if self.is_fallback_only(language):
-            # Only send response when fallback is among its sources
-            if 'fallback' in request_responses['sources']:
-                self.gather_and_send_to_codeeditor(request_responses)
-                return
-
-            if 'snippets' in request_responses['sources']:
-                self.gather_and_send_to_codeeditor(request_responses)
-                return
-
-            # This drops responses that don't contain fallback
-            return
-
-        # Wait between the LSP and Kite to return responses. This favors
-        # Kite when the LSP takes too much time to respond.
-        wait_for = set(source for source
-                       in self.WAIT_FOR_SOURCE[request_responses['req_type']]
-                       if self.is_client_running(source))
-        timed_out = request_responses['timed_out']
-        all_returned = all(source in request_responses['sources']
-                           for source in wait_for)
-
-        if not timed_out:
-            # Before the timeout
-            if all_returned:
-                self.skip_and_send_to_codeeditor(req_id)
-        else:
-            # After the timeout
-            any_nonempty = any(request_responses['sources'].get(source)
-                               for source in wait_for)
-            if all_returned or any_nonempty:
-                self.skip_and_send_to_codeeditor(req_id)
-
-    def skip_and_send_to_codeeditor(self, req_id):
-        """
-        Skip intermediate responses coming from the same CodeEditor
-        instance for some types of requests, and send the last one to
-        it.
-        """
-        request_responses = self.requests[req_id]
-        req_type = request_responses['req_type']
-        response_instance = id(request_responses['response_instance'])
-        do_send = True
-
-        # This is necessary to prevent sending completions for old requests
-        # See spyder-ide/spyder#10798
-        if req_type in self.SKIP_INTERMEDIATE_REQUESTS:
-            max_req_id = max(
-                [key for key, item in self.requests.items()
-                 if item['req_type'] == req_type
-                 and id(item['response_instance']) == response_instance]
-                or [-1])
-            do_send = (req_id == max_req_id)
-
-        logger.debug("Completion plugin: Request {} removed".format(req_id))
-        del self.requests[req_id]
-
-        # Send only recent responses
-        if do_send:
-            self.gather_and_send_to_codeeditor(request_responses)
-
-    def is_fallback_only(self, language):
-        """
-        Detect if fallback is the only client that works for language.
-        """
-        lang_status = self.language_status.get(language, {})
-        if lang_status:
-            if (not lang_status.get('lsp', {}) and
-                    not lang_status.get('kite', {})):
-                return True
-        return False
-
-    @Slot(str)
-    def client_available(self, client_name):
-        client_info = self.clients[client_name]
-        client_info['status'] = self.RUNNING
-
-    def gather_completions(self, req_id_responses):
-        """Gather completion responses from plugins."""
-        priorities = self.SOURCE_PRIORITY[LSPRequestTypes.DOCUMENT_COMPLETION]
-
-        merge_stats = {source: 0 for source in req_id_responses}
-        responses = []
-        dedupe_set = set()
-        for priority, source in enumerate(priorities):
-            if source not in req_id_responses:
-                continue
-            for response in req_id_responses[source].get('params', []):
-                dedupe_key = response['label'].strip()
-                if dedupe_key in dedupe_set:
-                    continue
-                dedupe_set.add(dedupe_key)
-
-                response['sortText'] = (priority, response['sortText'])
-                responses.append(response)
-                merge_stats[source] += 1
-
-        logger.debug('Responses statistics: {0}'.format(merge_stats))
-        responses = {'params': responses}
-        return responses
-
-    def gather_responses(self, req_type, responses):
-        """Gather responses other than completions from plugins."""
-        response = None
-        for source in self.SOURCE_PRIORITY[req_type]:
-            if source in responses:
-                response = responses[source].get('params', None)
-                if response:
-                    break
-        return {'params': response}
-
-    def gather_and_send_to_codeeditor(self, request_responses):
-        """
-        Gather request responses from all plugins and send them to the
-        CodeEditor instance that requested them.
-        """
-        req_type = request_responses['req_type']
-        req_id_responses = request_responses['sources']
-        response_instance = request_responses['response_instance']
-        logger.debug('Gather responses for {0}'.format(req_type))
-
-        if req_type == LSPRequestTypes.DOCUMENT_COMPLETION:
-            responses = self.gather_completions(req_id_responses)
-        else:
-            responses = self.gather_responses(req_type, req_id_responses)
-
-        try:
-            response_instance.handle_response(req_type, responses)
-        except RuntimeError:
-            # This is triggered when a codeeditor instance has been
-            # removed before the response can be processed.
-            pass
-
-    def is_client_running(self, name):
-        if name == LanguageServerPlugin.COMPLETION_CLIENT_NAME:
-            # The LSP plugin does not emit a plugin ready signal
-            return name in self.clients
-
-        status = self.clients.get(name, {}).get('status', self.STOPPED)
-        return status == self.RUNNING
-
-    def send_request(self, language, req_type, req):
-        req_id = self.req_id
-        self.req_id += 1
-
-        self.requests[req_id] = {
-            'language': language,
-            'req_type': req_type,
-            'response_instance': req['response_instance'],
-            'sources': {},
-            'timed_out': False,
-        }
-
-        # Start the timer on this request
-        if self.wait_for_ms > 0:
-            QTimer.singleShot(self.wait_for_ms,
-                              lambda: self.receive_timeout(req_id))
-        else:
-            self.requests[req_id]['timed_out'] = True
-
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            client_info['plugin'].send_request(
-                language, req_type, req, req_id)
-
-    def send_notification(self, language, notification_type, notification):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].send_notification(
-                    language, notification_type, notification)
-
-    def broadcast_notification(self, req_type, req):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].broadcast_notification(
-                    req_type, req)
-
-    def project_path_update(self, project_path, update_kind='addition'):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].project_path_update(
-                    project_path, update_kind
-                )
-
-    def register_file(self, language, filename, codeeditor):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].register_file(
-                    language, filename, codeeditor
-                )
-
-    def update_configuration(self):
-        self.wait_for_ms = self.get_option('completions_wait_for_ms',
-                                           section='editor')
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].update_configuration()
-
-    def start(self):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.STOPPED:
-                client_info['plugin'].start()
-
-    def shutdown(self):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].shutdown()
-
-    def start_client(self, language):
-        started = False
-        language_clients = self.language_status.get(language, {})
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_started = client_info['plugin'].start_client(language)
-                started |= client_started
-                language_clients[client_name] = client_started
-        self.language_status[language] = language_clients
-        return started
-
-    def stop_client(self, language):
-        for client_name in self.clients:
-            client_info = self.clients[client_name]
-            if client_info['status'] == self.RUNNING:
-                client_info['plugin'].stop_client(language)
-        self.language_status.pop(language)
-
-    def get_client(self, name):
-        return self.clients[name]['plugin']
-
-    def closing_plugin(self, cancelable=False):
-        """
-        Check state of the clients before closing.
-
-        Particularly for Kite, we need to check if an installation
-        is taking place.
-        """
-        kite_plugin = self.get_client('kite')
-        if cancelable and kite_plugin.is_installing():
-            reply = QMessageBox.critical(
-                self.main, 'Spyder',
-                _('Kite installation process has not finished. '
-                  'Do you really want to exit?'),
-                QMessageBox.Yes, QMessageBox.No)
-            if reply == QMessageBox.No:
-                return False
-        return True
diff --git a/spyder/plugins/completion/plugin.py b/spyder/plugins/completion/plugin.py
new file mode 100644
index 00000000000..3cb3935f3fb
--- /dev/null
+++ b/spyder/plugins/completion/plugin.py
@@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
+                logger.warning('Failed to load completion provider from entry '
+                               f'point {entry_point}')
+                raise e
+
+        # Define configuration page and tabs
+        conf_providers = []
+        conf_tabs = []
+        container = self.get_container()
+
+        for provider_key in self.providers:
+            provider = self.providers[provider_key]['instance']
+            for tab in provider.CONF_TABS:
+                # Add set_option/get_option/remove_option to tab definition
+                setattr(tab, 'get_option', wrap_get_option(provider_key))
+                setattr(tab, 'set_option', wrap_set_option(provider_key))
+                setattr(tab, 'remove_option', wrap_remove_option(provider_key))
+                # Wrap apply_settings to return settings correctly
+                setattr(tab, 'apply_settings',
+                        wrap_apply_settings(tab, provider_key))
+                # Wrap create_* methods to consider provider
+                for name, pos in widget_funcs:
+                    setattr(tab, name, wrap_create_op(name, pos, provider_key))
+            conf_tabs += provider.CONF_TABS
+            # Add defined status bars to container
+            container.register_statusbars(provider.STATUS_BARS)
+            conf_providers.append((provider_key, provider.get_name()))
+
+        self.CONF_WIDGET_CLASS = partialclass(
+            CompletionConfigPage, providers=conf_providers)
+        self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs}
+
+    # ---------------- Public Spyder API required methods ---------------------
+    def get_name(self) -> str:
+        return _('Completion and linting')
+
+    def get_description(self) -> str:
+        return _('This plugin is on charge of handling and dispatching, as '
+                 'well of receiving the responses of completion and linting '
+                 'requests sent to multiple providers.')
+
+    def get_icon(self):
+        return self.create_icon('lspserver')
+
+    def register(self):
+        """Start all available completion providers."""
+        preferences = self.get_plugin(Plugins.Preferences)
+        preferences.register_plugin_preferences(self)
+
+        container = self.get_container()
+        statusbar = self.get_plugin(Plugins.StatusBar)
+        if statusbar:
+            for sb in container.all_statusbars():
+                statusbar.add_status_widget(sb, 0)
+
+        # TODO: Move this section to the new API once the migration of
+        # Application is complete
+        if self.main:
+            self.main.sig_pythonpath_changed.connect(
+                self.sig_pythonpath_changed)
+
+        # TODO: Move this section to the new API once the migration of projects
+        # is complete
+        if self.main.projects:
+            pass
+
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.STOPPED:
+                # TODO: Register status bar widgets
+                provider_enabled = self.get_conf_option(
+                    ('enabled_providers', provider_name), True)
+                if provider_enabled:
+                    provider_info['instance'].start()
+
+    def unregister(self):
+        """Stop all running completion providers."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                # TODO: Remove status bar widgets
+                provider_info['instance'].shutdown()
+
+    def on_close(self, cancelable=False) -> bool:
+        """Check if any provider has any pending task before closing."""
+        can_close = False
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_can_close = provider.can_close()
+                can_close |= provider_can_close
+                if provider_can_close:
+                    provider.shutdown()
+        return can_close
+
+    def after_configuration_update(self, options: List[Union[tuple, str]]):
+        providers_to_update = set({})
+        for option in options:
+            if option == 'completions_wait_for_ms':
+                self.wait_for_ms = self.get_conf_option(
+                    'completions_wait_for_ms')
+            elif isinstance(option, tuple):
+                option_name, provider_name, *__ = option
+                if option_name == 'enabled_providers':
+                    provider_status = self.get_conf_option(
+                        ('enabled_providers', provider_name))
+                    if provider_status:
+                        self.start_provider_instance(provider_name)
+                    else:
+                        self.shutdown_provider_instance(provider_name)
+                elif option_name == 'provider_configuration':
+                    providers_to_update |= {provider_name}
+
+        for provider in providers_to_update:
+            self.update_provider_configuration(provider)
+
+    # ---------- Completion provider registering/start/stop methods -----------
+    def _merge_default_configurations(self,
+                                      Provider: SpyderCompletionProvider,
+                                      provider_name: str,
+                                      provider_configurations: dict):
+        provider_defaults = dict(Provider.CONF_DEFAULTS)
+        provider_conf_version = Provider.CONF_VERSION
+        if provider_name not in provider_configurations:
+            # Pick completion provider default configuration options
+            provider_config = {
+                'version': provider_conf_version,
+                'values': provider_defaults,
+                'defaults': provider_defaults,
+            }
+
+            provider_configurations[provider_name] = provider_config
+
+        # Check if there were any version changes between configurations
+        provider_config = provider_configurations[provider_name]
+        provider_conf_version = parse_version(Provider.CONF_VERSION)
+        current_conf_version = parse_version(provider_config['version'])
+
+        current_conf_values = provider_config['values']
+        current_defaults = provider_config['defaults']
+
+        # Check if there are new default values and copy them
+        new_keys = provider_defaults.keys() - current_conf_values.keys()
+        for new_key in new_keys:
+            current_conf_values[new_key] = provider_defaults[new_key]
+            current_defaults[new_key] = provider_defaults[new_key]
+
+        if provider_conf_version > current_conf_version:
+            # Check if default values were changed between versions,
+            # causing an overwrite of the current options
+            preserved_keys = current_defaults.keys() & provider_defaults.keys()
+            for key in preserved_keys:
+                if current_defaults[key] != provider_defaults[key]:
+                    current_defaults[key] = provider_defaults[key]
+                    current_conf_values[key] = provider_defaults[key]
+
+            if provider_conf_version.major != current_conf_version.major:
+                # Check if keys were removed/renamed from the previous defaults
+                deleted_keys = (
+                    current_defaults.keys() - provider_defaults.keys())
+                for key in deleted_keys:
+                    current_defaults.pop(key)
+                    current_conf_values.pop(key)
+
+        return str(provider_conf_version), current_conf_values, provider_defaults
+
+    def _instantiate_and_register_provider(
+            self, Provider: SpyderCompletionProvider):
+        container = self.get_container()
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+
+        provider_name = Provider.COMPLETION_CLIENT_NAME
+        self._available_providers[provider_name] = Provider
+
+        logger.debug("Completion plugin: Registering {0}".format(
+            provider_name))
+
+        # Merge configuration settings between a provider defaults and
+        # the existing ones
+        (provider_conf_version,
+         current_conf_values,
+         provider_defaults) = self._merge_default_configurations(
+             Provider, provider_name, provider_configurations)
+
+        new_provider_config = {
+            'version': provider_conf_version,
+            'values': current_conf_values,
+            'defaults': provider_defaults
+        }
+        provider_configurations[provider_name] = new_provider_config
+
+        # Update provider configurations
+        self.set_conf_option('provider_configuration', provider_configurations)
+
+        # Merge and update source priority order
+        source_priorities = self.get_conf_option('request_priorities')
+        provider_priority = Provider.DEFAULT_ORDER
+
+        for request in COMPLETION_REQUESTS:
+            request_priorities = source_priorities.get(request, {})
+            if provider_name not in request_priorities:
+                request_priorities[provider_name] = provider_priority - 1
+            source_priorities[request] = request_priorities
+
+        self.source_priority = source_priorities
+        self.set_conf_option('request_priorities', source_priorities)
+
+        provider_instance = Provider(self, new_provider_config['values'])
+
+        # Signals
+        provider_instance.sig_provider_ready.connect(self.provider_available)
+        provider_instance.sig_response_ready.connect(self.receive_response)
+        provider_instance.sig_exception_occurred.connect(
+            self.sig_exception_occurred)
+        provider_instance.sig_language_client_available.connect(
+            self.sig_language_client_available)
+        provider_instance.sig_show_widget.connect(
+            container.show_widget
+        )
+        provider_instance.sig_update_statusbar.connect(
+            container.statusbar_rpc)
+
+        self.sig_pythonpath_changed.connect(
+            provider_instance.python_path_update)
+        self.sig_main_interpreter_changed.connect(
+            provider_instance.main_interpreter_changed)
+
+        self.providers[provider_name] = {
+            'instance': provider_instance,
+            'status': self.STOPPED
+        }
+
+        for language in self.language_status:
+            server_status = self.language_status[language]
+            server_status[provider_name] = False
+
+    @Slot(str)
+    def provider_available(self, provider_name: str):
+        """Indicate that the completion provider `provider_name` is running."""
+        provider_info = self.providers[provider_name]
+        provider_info['status'] = self.RUNNING
+        self.sig_provider_ready.emit(provider_name)
+
+    def start_provider(self, language: str) -> bool:
+        """Start completion providers for a given programming language."""
+        started = False
+        language_providers = self.language_status.get(language, {})
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_started = provider.start_provider(language)
+                started |= provider_started
+                language_providers[provider_name] = provider_started
+        self.language_status[language] = language_providers
+        return started
+
+    def stop_provider(self, language: str):
+        """Stop completion providers for a given programming language."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].stop_provider(language)
+        self.language_status.pop(language)
+
+    def get_provider(self, name: str) -> SpyderCompletionProvider:
+        """Get the :class:`SpyderCompletionProvider` identified with `name`."""
+        return self.providers[name]['instance']
+
+    def is_provider_running(self, name: str) -> bool:
+        # TODO: Let LSP server emit sig_provider_ready
+        # if name == LanguageServerPlugin.COMPLETION_CLIENT_NAME:
+        #     # The LSP plugin does not emit a plugin ready signal
+        #     return name in self.clients
+
+        status = self.clients.get(name, {}).get('status', self.STOPPED)
+        return status == self.RUNNING
+
+    def available_providers_for_language(self, language: str) -> List[str]:
+        providers = []
+        if language in self.language_status:
+            provider_status = self.language_status[language]
+            providers = [p for p in provider_status if provider_status[p]]
+        return providers
+
+    def sort_providers_for_request(
+            self, providers: List[str], req_type: str) -> List[str]:
+        request_order = self.source_priority[req_type]
+        return sorted(providers, key=lambda p: request_order[p])
+
+    def start_provider_instance(self, provider_name: str):
+        """Start a given provider."""
+        provider_info = self.providers[provider_name]
+        if provider_info['status'] == self.STOPPED:
+            provider_info['instance'].start()
+
+    def shutdown_provider_instance(self, provider_name: str):
+        """Shutdown a given provider."""
+        provider_info = self.providers[provider_name]
+        if provider_info['status'] == self.RUNNING:
+            provider_info['instance'].shutdown()
+            provider_info['status'] = self.STOPPED
+
+    def update_provider_configuration(self, provider_name: str):
+        """Update a provider configuration."""
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+        provider_info = self.providers[provider_name]
+        if provider_info['status'] == self.RUNNING:
+            provider_configuration = provider_configurations[provider_name]
+            config = provider_configuration['values']
+            provider_info['instance'].update_configuration(config)
+
+    # --------------- Public completion API request methods -------------------
+    def send_request(self, language: str, req_type: str, req: dict):
+        """
+        Send a completion or linting request to all available providers.
+
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Name of the programming language of the file that emits the
+            request.
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        req_id = self.req_id
+        self.req_id += 1
+
+        self.requests[req_id] = {
+            'language': language,
+            'req_type': req_type,
+            'response_instance': req['response_instance'],
+            'sources': {},
+            'timed_out': False,
+        }
+
+        # Start the timer on this request
+        if req_type in self.AGGREGATE_RESPONSES:
+            if self.wait_for_ms > 0:
+                QTimer.singleShot(self.wait_for_ms,
+                                  lambda: self.receive_timeout(req_id))
+            else:
+                self.requests[req_id]['timed_out'] = True
+
+        # Send request to all running completion providers
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            provider_info['instance'].send_request(
+                language, req_type, req, req_id)
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send a notification to all available completion providers.
+
+        Parameters
+        ----------
+        language: str
+            Name of the programming language of the file that emits the
+            request.
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: notification-specific parameters
+            }
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].send_notification(
+                    language, notification_type, notification)
+
+    def broadcast_notification(self, req_type: str, req: dict):
+        """
+        Send a notification to all available completion providers for all
+        programming languages.
+
+        Parameters
+        ----------
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: notification-specific parameters
+            }
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].broadcast_notification(
+                    req_type, req)
+
+    def project_path_update(self, project_path: str, update_kind='addition',
+                            instance=None):
+        """
+        Handle project path updates on Spyder.
+
+        Parameters
+        ----------
+        project_path: str
+            Path to the project folder modified
+        update_kind: str
+            Path update kind, one of
+            :class:`spyder.plugins.completion.WorkspaceUpdateKind`
+        instance: object
+            Reference to :class:`spyder.plugins.projects.plugin.Projects`
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].project_path_update(
+                    project_path, update_kind, instance
+                )
+
+    @Slot(str, str)
+    def editor_focus_changed(
+            self, filename: Optional[str], language: Optional[str]):
+        """
+        Handle focus changes across tabs and split editors.
+
+        Parameters
+        ----------
+        filename: Optional[str]
+            Path to the file currently focused on the editor. If None, then
+            the focus is not currently on the editor.
+        language: Optional[str]
+            Name of the programming language of the currently focused file.
+            If None, then the focus is not currently on the editor.
+        """
+        if filename is not None and language is not None:
+            for provider_name in self.providers:
+                provider_info = self.providers[provider_name]
+                if provider_info['status'] == self.RUNNING:
+                    provider_info['instance'].editor_focus_changed(
+                        filename, language)
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].register_file(
+                    language, filename, codeeditor
+                )
+
+    def update_configuration(self):
+        """Handle completion option configuration updates."""
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+        for provider_name in self.providers:
+            self.update_provider_configuration(provider_name)
+
+    def get_global_option(
+            self, option: str, section: str, default: Any = None) -> Any:
+        """
+        Retrieve an option value from the global Spyder configurations.
+
+        Parameters
+        ----------
+        option: str
+            Option name to lookup for in global Spyder configurations.
+        default: Any
+            Default value to return if `option_name` was not found.
+        section: str
+            Name of the configuration section in Spyder
+
+        Returns
+        -------
+        Any
+            Either the default value if `option_name` was not found on the
+            settings or the actual value stored.
+        """
+        return self.get_conf_option(option, default=default, section=section)
+
+    # ----------------- Completion result processing methods ------------------
+    @Slot(str, int, dict)
+    def receive_response(
+            self, completion_source: str, req_id: int, resp: dict):
+        """Process request response from a completion provider."""
+        logger.debug("Completion plugin: Request {0} Got response "
+                     "from {1}".format(req_id, completion_source))
+
+        if req_id not in self.requests:
+            return
+
+        with QMutexLocker(self.collection_mutex):
+            request_responses = self.requests[req_id]
+            request_responses['sources'][completion_source] = resp
+            self.match_and_reply(req_id)
+
+    @Slot(int)
+    def receive_timeout(self, req_id: int):
+        """Collect all provider completions and reply on timeout."""
+        # On timeout, collect all completions and return to the user
+        if req_id not in self.requests:
+            return
+
+        logger.debug("Completion plugin: Request {} timed out".format(req_id))
+
+        with QMutexLocker(self.collection_mutex):
+            request_responses = self.requests[req_id]
+            request_responses['timed_out'] = True
+            self.match_and_reply(req_id)
+
+    def match_and_reply(self, req_id: int):
+        """
+        Decide how to send the responses corresponding to req_id to
+        the instance that requested them.
+        """
+        if req_id not in self.requests:
+            return
+        request_responses = self.requests[req_id]
+        language = request_responses['language'].lower()
+        req_type = request_responses['req_type']
+
+        if req_type in self.AGGREGATE_RESPONSES:
+            # Wait only for the available providers for the given request
+            available_providers = self.available_providers_for_language(
+                language)
+            sorted_providers = self.sort_providers_for_request(
+                available_providers, req_type)
+            timed_out = request_responses['timed_out']
+            all_returned = all(source in request_responses['sources']
+                            for source in sorted_providers)
+            if not timed_out:
+                # Before the timeout
+                if all_returned:
+                    self.skip_and_reply(req_id)
+            else:
+                # After the timeout
+                any_nonempty = any(request_responses['sources'].get(source)
+                                for source in sorted_providers)
+                if all_returned or any_nonempty:
+                    self.skip_and_reply(req_id)
+        else:
+            self.skip_and_reply(req_id)
+
+    def skip_and_reply(self, req_id: int):
+        """
+        Skip intermediate responses coming from the same CodeEditor
+        instance for some types of requests, and send the last one to
+        it.
+        """
+        request_responses = self.requests[req_id]
+        req_type = request_responses['req_type']
+        response_instance = id(request_responses['response_instance'])
+        do_send = True
+
+        # This is necessary to prevent sending completions for old requests
+        # See spyder-ide/spyder#10798
+        if req_type in self.SKIP_INTERMEDIATE_REQUESTS:
+            max_req_id = max(
+                [key for key, item in self.requests.items()
+                 if item['req_type'] == req_type
+                 and id(item['response_instance']) == response_instance]
+                or [-1])
+            do_send = (req_id == max_req_id)
+
+        logger.debug("Completion plugin: Request {} removed".format(req_id))
+        del self.requests[req_id]
+
+        # Send only recent responses
+        if do_send:
+            self.gather_and_reply(request_responses)
+
+    def gather_and_reply(self, request_responses: dict):
+        """
+        Gather request responses from all plugins and send them to the
+        CodeEditor instance that requested them.
+        """
+        req_type = request_responses['req_type']
+        req_id_responses = request_responses['sources']
+        response_instance = request_responses['response_instance']
+        logger.debug('Gather responses for {0}'.format(req_type))
+
+        if req_type == CompletionRequestTypes.DOCUMENT_COMPLETION:
+            responses = self.gather_completions(req_id_responses)
+        else:
+            responses = self.gather_responses(req_type, req_id_responses)
+
+        try:
+            response_instance.handle_response(req_type, responses)
+        except RuntimeError:
+            # This is triggered when a codeeditor instance has been
+            # removed before the response can be processed.
+            pass
+
+    def gather_completions(self, req_id_responses: dict):
+        """Gather completion responses from plugins."""
+        priorities = self.source_priority[
+            CompletionRequestTypes.DOCUMENT_COMPLETION]
+        priorities = sorted(list(priorities.keys()),
+                            key=lambda p: priorities[p])
+
+        merge_stats = {source: 0 for source in req_id_responses}
+        responses = []
+        dedupe_set = set()
+        for priority, source in enumerate(priorities):
+            if source not in req_id_responses:
+                continue
+            for response in req_id_responses[source].get('params', []):
+                dedupe_key = response['label'].strip()
+                if dedupe_key in dedupe_set:
+                    continue
+                dedupe_set.add(dedupe_key)
+
+                response['sortText'] = (priority, response['sortText'])
+                responses.append(response)
+                merge_stats[source] += 1
+
+        logger.debug('Responses statistics: {0}'.format(merge_stats))
+        responses = {'params': responses}
+        return responses
+
+    def gather_responses(self, req_type: int, responses: dict):
+        """Gather responses other than completions from plugins."""
+        response = None
+        for source in self.source_priority[req_type]:
+            if source in responses:
+                response = responses[source].get('params', None)
+                if response:
+                    break
+        return {'params': response}
diff --git a/spyder/plugins/completion/fallback/tests/__init__.py b/spyder/plugins/completion/providers/__init__.py
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/__init__.py
rename to spyder/plugins/completion/providers/__init__.py
diff --git a/spyder/plugins/completion/languageserver/tests/__init__.py b/spyder/plugins/completion/providers/fallback/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/tests/__init__.py
rename to spyder/plugins/completion/providers/fallback/__init__.py
diff --git a/spyder/plugins/completion/fallback/actor.py b/spyder/plugins/completion/providers/fallback/actor.py
similarity index 91%
rename from spyder/plugins/completion/fallback/actor.py
rename to spyder/plugins/completion/providers/fallback/actor.py
index 7df55689a6f..920f2377eed 100644
--- a/spyder/plugins/completion/fallback/actor.py
+++ b/spyder/plugins/completion/providers/fallback/actor.py
@@ -24,9 +24,9 @@
 from diff_match_patch import diff_match_patch
 
 # Local imports
-from spyder.plugins.completion.manager.api import CompletionItemKind
-from spyder.plugins.completion.manager.api import LSPRequestTypes
-from spyder.plugins.completion.fallback.utils import (
+from spyder.plugins.completion.api import CompletionItemKind
+from spyder.plugins.completion.api import CompletionRequestTypes
+from spyder.plugins.completion.providers.fallback.utils import (
     get_keywords, get_words, is_prefix_valid)
 
 
@@ -122,13 +122,13 @@ def handle_msg(self, message):
         msg_type, _id, file, msg = [
             message[k] for k in ('type', 'id', 'file', 'msg')]
         logger.debug(u'Perform request {0} with id {1}'.format(msg_type, _id))
-        if msg_type == LSPRequestTypes.DOCUMENT_DID_OPEN:
+        if msg_type == CompletionRequestTypes.DOCUMENT_DID_OPEN:
             self.file_tokens[file] = {
                 'text': msg['text'],
                 'offset': msg['offset'],
                 'language': msg['language'],
             }
-        elif msg_type == LSPRequestTypes.DOCUMENT_DID_CHANGE:
+        elif msg_type == CompletionRequestTypes.DOCUMENT_DID_CHANGE:
             if file not in self.file_tokens:
                 self.file_tokens[file] = {
                     'text': '',
@@ -141,9 +141,9 @@ def handle_msg(self, message):
             text, _ = self.diff_patch.patch_apply(
                 diff, text['text'])
             self.file_tokens[file]['text'] = text
-        elif msg_type == LSPRequestTypes.DOCUMENT_DID_CLOSE:
+        elif msg_type == CompletionRequestTypes.DOCUMENT_DID_CLOSE:
             self.file_tokens.pop(file, {})
-        elif msg_type == LSPRequestTypes.DOCUMENT_COMPLETION:
+        elif msg_type == CompletionRequestTypes.DOCUMENT_COMPLETION:
             tokens = []
             if file in self.file_tokens:
                 text_info = self.file_tokens[file]
diff --git a/spyder/plugins/completion/fallback/plugin.py b/spyder/plugins/completion/providers/fallback/provider.py
similarity index 62%
rename from spyder/plugins/completion/fallback/plugin.py
rename to spyder/plugins/completion/providers/fallback/provider.py
index f3cc9c9306d..9b5f05e4287 100644
--- a/spyder/plugins/completion/fallback/plugin.py
+++ b/spyder/plugins/completion/providers/fallback/provider.py
@@ -5,54 +5,57 @@
 # (see spyder/__init__.py for details)
 
 """
-Fallback completion plugin.
+Fallback completion provider.
 
-Wraps FallbackActor to provide compatibility with SpyderCompletionPlugin API.
+Wraps FallbackActor to provide compatibility with SpyderCompletionProvider API.
 """
 
 # Standard library imports
 import logging
 
 # Local imports
-from spyder.plugins.completion.manager.api import SpyderCompletionPlugin
-from spyder.plugins.completion.fallback.actor import FallbackActor
+from spyder.config.base import _
+from spyder.plugins.completion.api import SpyderCompletionProvider
+from spyder.plugins.completion.providers.fallback.actor import FallbackActor
 
 
 logger = logging.getLogger(__name__)
 
 
-class FallbackPlugin(SpyderCompletionPlugin):
-    CONF_SECTION = 'fallback-completions'
-    CONF_FILE = False
+class FallbackProvider(SpyderCompletionProvider):
     COMPLETION_CLIENT_NAME = 'fallback'
+    DEFAULT_ORDER = 3
 
-    def __init__(self, parent):
-        SpyderCompletionPlugin.__init__(self, parent)
+    def __init__(self, parent, config):
+        SpyderCompletionProvider.__init__(self, parent, config)
         self.fallback_actor = FallbackActor(self)
         self.fallback_actor.sig_fallback_ready.connect(
-            lambda: self.sig_plugin_ready.emit(self.COMPLETION_CLIENT_NAME))
+            lambda: self.sig_provider_ready.emit(self.COMPLETION_CLIENT_NAME))
         self.fallback_actor.sig_set_tokens.connect(
             lambda _id, resp: self.sig_response_ready.emit(
                 self.COMPLETION_CLIENT_NAME, _id, resp))
         self.started = False
         self.requests = {}
-        self.update_configuration()
 
-    def start_client(self, language):
+    def get_name(self):
+        return _('Fallback')
+
+    def start_provider(self, language):
         return self.started
 
     def start(self):
-        if not self.started and self.enabled:
+        if not self.started:
             self.fallback_actor.start()
             self.started = True
 
     def shutdown(self):
         if self.started:
             self.fallback_actor.stop()
+            self.started = False
 
     def send_request(self, language, req_type, req, req_id=None):
-        if not self.enabled:
-            return
+        # if not self.enabled:
+        #     return
 
         request = {
             'type': req_type,
@@ -63,6 +66,5 @@ def send_request(self, language, req_type, req, req_id=None):
         req['language'] = language
         self.fallback_actor.sig_mailbox.emit(request)
 
-    def update_configuration(self):
-        self.enabled = self.get_option('enable')
-        self.start()
+    def can_close(self):
+        return True
diff --git a/spyder/plugins/completion/languageserver/transport/common/__init__.py b/spyder/plugins/completion/providers/fallback/tests/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/transport/common/__init__.py
rename to spyder/plugins/completion/providers/fallback/tests/__init__.py
diff --git a/spyder/plugins/completion/fallback/tests/conftest.py b/spyder/plugins/completion/providers/fallback/tests/conftest.py
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/conftest.py
rename to spyder/plugins/completion/providers/fallback/tests/conftest.py
diff --git a/spyder/plugins/completion/fallback/tests/data/example.R b/spyder/plugins/completion/providers/fallback/tests/data/example.R
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.R
rename to spyder/plugins/completion/providers/fallback/tests/data/example.R
diff --git a/spyder/plugins/completion/fallback/tests/data/example.c b/spyder/plugins/completion/providers/fallback/tests/data/example.c
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.c
rename to spyder/plugins/completion/providers/fallback/tests/data/example.c
diff --git a/spyder/plugins/completion/fallback/tests/data/example.cpp b/spyder/plugins/completion/providers/fallback/tests/data/example.cpp
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.cpp
rename to spyder/plugins/completion/providers/fallback/tests/data/example.cpp
diff --git a/spyder/plugins/completion/fallback/tests/data/example.css b/spyder/plugins/completion/providers/fallback/tests/data/example.css
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.css
rename to spyder/plugins/completion/providers/fallback/tests/data/example.css
diff --git a/spyder/plugins/completion/fallback/tests/data/example.erl b/spyder/plugins/completion/providers/fallback/tests/data/example.erl
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.erl
rename to spyder/plugins/completion/providers/fallback/tests/data/example.erl
diff --git a/spyder/plugins/completion/fallback/tests/data/example.ex b/spyder/plugins/completion/providers/fallback/tests/data/example.ex
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.ex
rename to spyder/plugins/completion/providers/fallback/tests/data/example.ex
diff --git a/spyder/plugins/completion/fallback/tests/data/example.html b/spyder/plugins/completion/providers/fallback/tests/data/example.html
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.html
rename to spyder/plugins/completion/providers/fallback/tests/data/example.html
diff --git a/spyder/plugins/completion/fallback/tests/data/example.java b/spyder/plugins/completion/providers/fallback/tests/data/example.java
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.java
rename to spyder/plugins/completion/providers/fallback/tests/data/example.java
diff --git a/spyder/plugins/completion/fallback/tests/data/example.jl b/spyder/plugins/completion/providers/fallback/tests/data/example.jl
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.jl
rename to spyder/plugins/completion/providers/fallback/tests/data/example.jl
diff --git a/spyder/plugins/completion/fallback/tests/data/example.md b/spyder/plugins/completion/providers/fallback/tests/data/example.md
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.md
rename to spyder/plugins/completion/providers/fallback/tests/data/example.md
diff --git a/spyder/plugins/completion/fallback/tests/data/example.py b/spyder/plugins/completion/providers/fallback/tests/data/example.py
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example.py
rename to spyder/plugins/completion/providers/fallback/tests/data/example.py
diff --git a/spyder/plugins/completion/fallback/tests/data/example_latin1.py b/spyder/plugins/completion/providers/fallback/tests/data/example_latin1.py
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/example_latin1.py
rename to spyder/plugins/completion/providers/fallback/tests/data/example_latin1.py
diff --git a/spyder/plugins/completion/fallback/tests/data/tokens.json b/spyder/plugins/completion/providers/fallback/tests/data/tokens.json
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/data/tokens.json
rename to spyder/plugins/completion/providers/fallback/tests/data/tokens.json
diff --git a/spyder/plugins/completion/fallback/tests/test_fallback.py b/spyder/plugins/completion/providers/fallback/tests/test_fallback.py
similarity index 100%
rename from spyder/plugins/completion/fallback/tests/test_fallback.py
rename to spyder/plugins/completion/providers/fallback/tests/test_fallback.py
diff --git a/spyder/plugins/completion/fallback/utils.py b/spyder/plugins/completion/providers/fallback/utils.py
similarity index 100%
rename from spyder/plugins/completion/fallback/utils.py
rename to spyder/plugins/completion/providers/fallback/utils.py
diff --git a/spyder/plugins/completion/kite/__init__.py b/spyder/plugins/completion/providers/kite/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/__init__.py
rename to spyder/plugins/completion/providers/kite/__init__.py
diff --git a/spyder/plugins/completion/kite/bloomfilter/__init__.py b/spyder/plugins/completion/providers/kite/bloomfilter/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/bloomfilter/__init__.py
rename to spyder/plugins/completion/providers/kite/bloomfilter/__init__.py
diff --git a/spyder/plugins/completion/kite/bloomfilter/kite.bloom b/spyder/plugins/completion/providers/kite/bloomfilter/kite.bloom
similarity index 100%
rename from spyder/plugins/completion/kite/bloomfilter/kite.bloom
rename to spyder/plugins/completion/providers/kite/bloomfilter/kite.bloom
diff --git a/spyder/plugins/completion/kite/client.py b/spyder/plugins/completion/providers/kite/client.py
similarity index 100%
rename from spyder/plugins/completion/kite/client.py
rename to spyder/plugins/completion/providers/kite/client.py
diff --git a/spyder/plugins/completion/kite/decorators.py b/spyder/plugins/completion/providers/kite/decorators.py
similarity index 100%
rename from spyder/plugins/completion/kite/decorators.py
rename to spyder/plugins/completion/providers/kite/decorators.py
diff --git a/spyder/plugins/completion/kite/parsing/__init__.py b/spyder/plugins/completion/providers/kite/parsing/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/parsing/__init__.py
rename to spyder/plugins/completion/providers/kite/parsing/__init__.py
diff --git a/spyder/plugins/completion/kite/parsing/tests/__init__.py b/spyder/plugins/completion/providers/kite/parsing/tests/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/parsing/tests/__init__.py
rename to spyder/plugins/completion/providers/kite/parsing/tests/__init__.py
diff --git a/spyder/plugins/completion/kite/parsing/tests/test_parsing.py b/spyder/plugins/completion/providers/kite/parsing/tests/test_parsing.py
similarity index 100%
rename from spyder/plugins/completion/kite/parsing/tests/test_parsing.py
rename to spyder/plugins/completion/providers/kite/parsing/tests/test_parsing.py
diff --git a/spyder/plugins/completion/kite/plugin.py b/spyder/plugins/completion/providers/kite/plugin.py
similarity index 100%
rename from spyder/plugins/completion/kite/plugin.py
rename to spyder/plugins/completion/providers/kite/plugin.py
diff --git a/spyder/plugins/completion/kite/providers/__init__.py b/spyder/plugins/completion/providers/kite/providers/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/providers/__init__.py
rename to spyder/plugins/completion/providers/kite/providers/__init__.py
diff --git a/spyder/plugins/completion/kite/providers/document.py b/spyder/plugins/completion/providers/kite/providers/document.py
similarity index 100%
rename from spyder/plugins/completion/kite/providers/document.py
rename to spyder/plugins/completion/providers/kite/providers/document.py
diff --git a/spyder/plugins/completion/kite/utils/__init__.py b/spyder/plugins/completion/providers/kite/utils/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/utils/__init__.py
rename to spyder/plugins/completion/providers/kite/utils/__init__.py
diff --git a/spyder/plugins/completion/kite/utils/install.py b/spyder/plugins/completion/providers/kite/utils/install.py
similarity index 100%
rename from spyder/plugins/completion/kite/utils/install.py
rename to spyder/plugins/completion/providers/kite/utils/install.py
diff --git a/spyder/plugins/completion/kite/utils/status.py b/spyder/plugins/completion/providers/kite/utils/status.py
similarity index 100%
rename from spyder/plugins/completion/kite/utils/status.py
rename to spyder/plugins/completion/providers/kite/utils/status.py
diff --git a/spyder/plugins/completion/kite/utils/tests/__init__.py b/spyder/plugins/completion/providers/kite/utils/tests/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/utils/tests/__init__.py
rename to spyder/plugins/completion/providers/kite/utils/tests/__init__.py
diff --git a/spyder/plugins/completion/kite/utils/tests/test_install.py b/spyder/plugins/completion/providers/kite/utils/tests/test_install.py
similarity index 100%
rename from spyder/plugins/completion/kite/utils/tests/test_install.py
rename to spyder/plugins/completion/providers/kite/utils/tests/test_install.py
diff --git a/spyder/plugins/completion/kite/widgets/__init__.py b/spyder/plugins/completion/providers/kite/widgets/__init__.py
similarity index 100%
rename from spyder/plugins/completion/kite/widgets/__init__.py
rename to spyder/plugins/completion/providers/kite/widgets/__init__.py
diff --git a/spyder/plugins/completion/kite/widgets/calltoaction.py b/spyder/plugins/completion/providers/kite/widgets/calltoaction.py
similarity index 100%
rename from spyder/plugins/completion/kite/widgets/calltoaction.py
rename to spyder/plugins/completion/providers/kite/widgets/calltoaction.py
diff --git a/spyder/plugins/completion/kite/widgets/install.py b/spyder/plugins/completion/providers/kite/widgets/install.py
similarity index 100%
rename from spyder/plugins/completion/kite/widgets/install.py
rename to spyder/plugins/completion/providers/kite/widgets/install.py
diff --git a/spyder/plugins/completion/kite/widgets/status.py b/spyder/plugins/completion/providers/kite/widgets/status.py
similarity index 100%
rename from spyder/plugins/completion/kite/widgets/status.py
rename to spyder/plugins/completion/providers/kite/widgets/status.py
diff --git a/spyder/plugins/completion/languageserver/__init__.py b/spyder/plugins/completion/providers/languageserver/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/__init__.py
rename to spyder/plugins/completion/providers/languageserver/__init__.py
diff --git a/spyder/plugins/completion/languageserver/client.py b/spyder/plugins/completion/providers/languageserver/client.py
similarity index 97%
rename from spyder/plugins/completion/languageserver/client.py
rename to spyder/plugins/completion/providers/languageserver/client.py
index 1ac75770b05..5774155477b 100644
--- a/spyder/plugins/completion/languageserver/client.py
+++ b/spyder/plugins/completion/providers/languageserver/client.py
@@ -28,14 +28,14 @@
 # Local imports
 from spyder.config.base import (DEV, get_conf_path, get_debug_level,
                                 running_under_pytest)
-from spyder.plugins.completion.manager.api import (
+from spyder.plugins.completion.api import (
     CLIENT_CAPABILITES, SERVER_CAPABILITES,
-    TEXT_DOCUMENT_SYNC_OPTIONS, LSPRequestTypes,
+    TEXT_DOCUMENT_SYNC_OPTIONS, CompletionRequestTypes,
     ClientConstants)
-from spyder.plugins.completion.languageserver.decorators import (
+from spyder.plugins.completion.providers.languageserver.decorators import (
     send_request, send_notification, class_register, handles)
-from spyder.plugins.completion.languageserver.transport import MessageKind
-from spyder.plugins.completion.languageserver.providers import (
+from spyder.plugins.completion.providers.languageserver.transport import MessageKind
+from spyder.plugins.completion.providers.languageserver.providers import (
     LSPMethodProviderMixIn)
 from spyder.py3compat import PY2
 from spyder.utils.misc import getcwd_or_home, select_port
@@ -552,7 +552,7 @@ def perform_request(self, method, params):
 
     # ------ LSP initialization methods --------------------------------
     @handles(SERVER_READY)
-    @send_request(method=LSPRequestTypes.INITIALIZE)
+    @send_request(method=CompletionRequestTypes.INITIALIZE)
     def initialize(self, params, *args, **kwargs):
         self.stdio_pid = params['pid']
         pid = self.transport.processId() if not self.external_server else None
@@ -564,21 +564,21 @@ def initialize(self, params, *args, **kwargs):
         }
         return params
 
-    @send_request(method=LSPRequestTypes.SHUTDOWN)
+    @send_request(method=CompletionRequestTypes.SHUTDOWN)
     def shutdown(self):
         params = {}
         return params
 
-    @handles(LSPRequestTypes.SHUTDOWN)
+    @handles(CompletionRequestTypes.SHUTDOWN)
     def handle_shutdown(self, response, *args):
         self.ready_to_close = True
 
-    @send_notification(method=LSPRequestTypes.EXIT)
+    @send_notification(method=CompletionRequestTypes.EXIT)
     def exit(self):
         params = {}
         return params
 
-    @handles(LSPRequestTypes.INITIALIZE)
+    @handles(CompletionRequestTypes.INITIALIZE)
     def process_server_capabilities(self, server_capabilites, *args):
         """
         Register server capabilities and inform other plugins that it's
@@ -608,7 +608,7 @@ def process_server_capabilities(self, server_capabilites, *args):
         # Inform other plugins that the server is up.
         self.sig_initialize.emit(self.server_capabilites, self.language)
 
-    @send_notification(method=LSPRequestTypes.INITIALIZED)
+    @send_notification(method=CompletionRequestTypes.INITIALIZED)
     def initialized_call(self):
         params = {}
         return params
diff --git a/spyder/plugins/completion/languageserver/confpage.py b/spyder/plugins/completion/providers/languageserver/confpage.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/confpage.py
rename to spyder/plugins/completion/providers/languageserver/confpage.py
diff --git a/spyder/plugins/completion/providers/languageserver/conftabs.py b/spyder/plugins/completion/providers/languageserver/conftabs.py
new file mode 100644
index 00000000000..93f2216595c
--- /dev/null
+++ b/spyder/plugins/completion/providers/languageserver/conftabs.py
@@ -0,0 +1,611 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Language Server Protocol configuration tabs.
+"""
+
+# Standard library imports
+import copy
+import os.path as osp
+import re
+import sys
+
+# Third party imports
+from qtpy.compat import getsavefilename, getopenfilename
+from qtpy.QtCore import Qt, Slot
+from qtpy.QtWidgets import (QComboBox, QGroupBox, QGridLayout, QLabel,
+                            QMessageBox, QPushButton, QTabWidget, QVBoxLayout,
+                            QWidget, QFileDialog)
+
+# Local imports
+from spyder.api.preferences import SpyderPreferencesTab
+from spyder.config.base import _
+from spyder.config.manager import CONF
+from spyder.config.gui import is_dark_interface
+from spyder.plugins.completion.api import SUPPORTED_LANGUAGES
+from spyder.plugins.completion.providers.languageserver.widgets.serversconfig import (
+    LSPServerTable)
+# from spyder.plugins.completion.languageserver.widgets.snippetsconfig import (
+#     SnippetModelsProxy, SnippetTable, LSP_LANGUAGES_PY, PYTHON_POS)
+# from spyder.plugins.completion.manager.api import LSP_LANGUAGES
+from spyder.plugins.preferences.api import GeneralConfigPage
+from spyder.utils import icon_manager as ima
+from spyder.utils.misc import check_connection_port
+
+LSP_URL = "https://microsoft.github.io/language-server-protocol"
+
+
+class IntrospectionConfigTab(SpyderPreferencesTab):
+    """Introspection settings tab."""
+
+    TITLE = _('Introspection')
+    CTRL = "Cmd" if sys.platform == 'darwin' else "Ctrl"
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        newcb = self.create_checkbox
+
+        introspection_group = QGroupBox(_("Basic features"))
+        goto_definition_box = newcb(
+            _("Enable Go to definition"),
+            'jedi_definition',
+            tip=_("If enabled, left-clicking on an object name while \n"
+                  "pressing the {} key will go to that object's definition\n"
+                  "(if resolved).").format(self.CTRL))
+        follow_imports_box = newcb(_("Follow imports when going to a "
+                                     "definition"),
+                                   'jedi_definition/follow_imports')
+        show_signature_box = newcb(_("Show calltips"), 'jedi_signature_help')
+        enable_hover_hints_box = newcb(
+            _("Enable hover hints"),
+            'enable_hover_hints',
+            tip=_("If enabled, hovering the mouse pointer over an object\n"
+                  "name will display that object's signature and/or\n"
+                  "docstring (if present)."))
+        introspection_layout = QVBoxLayout()
+        introspection_layout.addWidget(goto_definition_box)
+        introspection_layout.addWidget(follow_imports_box)
+        introspection_layout.addWidget(show_signature_box)
+        introspection_layout.addWidget(enable_hover_hints_box)
+        introspection_group.setLayout(introspection_layout)
+
+        goto_definition_box.toggled.connect(follow_imports_box.setEnabled)
+
+        # Advanced group
+        advanced_group = QGroupBox(_("Advanced"))
+        modules_textedit = self.create_textedit(
+            _("Preload the following modules to make completion faster "
+              "and more accurate:"),
+            'preload_modules'
+        )
+        if is_dark_interface():
+            modules_textedit.textbox.setStyleSheet(
+                "border: 1px solid #32414B;"
+            )
+
+        advanced_layout = QVBoxLayout()
+        advanced_layout.addWidget(modules_textedit)
+        advanced_group.setLayout(advanced_layout)
+
+        layout = QVBoxLayout()
+        layout.addWidget(introspection_group)
+        layout.addWidget(advanced_group)
+        layout.addStretch(1)
+        self.setLayout(layout)
+
+
+class LintingConfigPage(SpyderPreferencesTab):
+    """Linting configuration tab."""
+
+    TITLE = _('Linting')
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        newcb = self.create_checkbox
+
+        linting_label = QLabel(_("Spyder can optionally highlight syntax "
+                                 "errors and possible problems with your "
+                                 "code in the editor."))
+        linting_label.setOpenExternalLinks(True)
+        linting_label.setWordWrap(True)
+        linting_check = self.create_checkbox(
+            _("Enable basic linting"),
+            'pyflakes')
+        underline_errors_box = newcb(
+            _("Underline errors and warnings"),
+            'underline_errors',
+            section='editor')
+        linting_complexity_box = self.create_checkbox(
+            _("Enable complexity linting with the Mccabe package"),
+            'mccabe')
+
+        # Linting layout
+        linting_layout = QVBoxLayout()
+        linting_layout.addWidget(linting_label)
+        linting_layout.addWidget(linting_check)
+        linting_layout.addWidget(underline_errors_box)
+        linting_layout.addWidget(linting_complexity_box)
+        self.setLayout(linting_layout)
+        linting_check.toggled.connect(underline_errors_box.setEnabled)
+
+
+class FormattingStyleConfigTab(SpyderPreferencesTab):
+    """Code style and formatting tab."""
+
+    TITLE = _('Code style and formatting')
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        newcb = self.create_checkbox
+
+        pep_url = (
+            '<a href="https://www.python.org/dev/peps/pep-0008">PEP 8</a>')
+        code_style_codes_url = _(
+            "<a href='http://pycodestyle.pycqa.org/en/stable"
+            "/intro.html#error-codes'>pycodestyle error codes</a>")
+        code_style_label = QLabel(
+            _("Spyder can use pycodestyle to analyze your code for "
+              "conformance to the {} convention. You can also "
+              "manually show or hide specific warnings by their "
+              "{}.").format(pep_url, code_style_codes_url))
+        code_style_label.setOpenExternalLinks(True)
+        code_style_label.setWordWrap(True)
+
+        # Code style checkbox
+        self.code_style_check = self.create_checkbox(
+            _("Enable code style linting"),
+            'pycodestyle')
+
+        # Code style options
+        self.code_style_filenames_match = self.create_lineedit(
+            _("Only check filenames matching these patterns:"),
+            'pycodestyle/filename', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Check Python files: *.py"))
+        self.code_style_exclude = self.create_lineedit(
+            _("Exclude files or directories matching these patterns:"),
+            'pycodestyle/exclude', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Exclude all test files: (?!test_).*\\.py"))
+        code_style_select = self.create_lineedit(
+            _("Show the following errors or warnings:").format(
+                code_style_codes_url),
+            'pycodestyle/select', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Example codes: E113, W391"))
+        code_style_ignore = self.create_lineedit(
+            _("Ignore the following errors or warnings:"),
+            'pycodestyle/ignore', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Example codes: E201, E303"))
+        self.code_style_max_line_length = self.create_spinbox(
+            _("Maximum allowed line length:"), None,
+            'pycodestyle/max_line_length', min_=10, max_=500, step=1,
+            tip=_("Default is 79"))
+
+        vertical_line_box = newcb(
+            _("Show vertical line at that length"), 'edge_line',
+            section='editor')
+
+        # Code style layout
+        code_style_g_layout = QGridLayout()
+        code_style_g_layout.addWidget(
+            self.code_style_filenames_match.label, 1, 0)
+        code_style_g_layout.addWidget(
+            self.code_style_filenames_match.textbox, 1, 1)
+        code_style_g_layout.addWidget(self.code_style_exclude.label, 2, 0)
+        code_style_g_layout.addWidget(self.code_style_exclude.textbox, 2, 1)
+        code_style_g_layout.addWidget(code_style_select.label, 3, 0)
+        code_style_g_layout.addWidget(code_style_select.textbox, 3, 1)
+        code_style_g_layout.addWidget(code_style_ignore.label, 4, 0)
+        code_style_g_layout.addWidget(code_style_ignore.textbox, 4, 1)
+
+        # Set Code style options enabled/disabled
+        code_style_g_widget = QWidget()
+        code_style_g_widget.setLayout(code_style_g_layout)
+        code_style_g_widget.setEnabled(self.get_option('pycodestyle'))
+        self.code_style_check.toggled.connect(code_style_g_widget.setEnabled)
+
+        # Code style layout
+        code_style_group = QGroupBox(_("Code style"))
+        code_style_layout = QVBoxLayout()
+        code_style_layout.addWidget(code_style_label)
+        code_style_layout.addWidget(self.code_style_check)
+        code_style_layout.addWidget(code_style_g_widget)
+        code_style_group.setLayout(code_style_layout)
+
+        # Maximum allowed line length layout
+        line_length_group = QGroupBox(_("Line length"))
+        line_length_layout = QVBoxLayout()
+        line_length_layout.addWidget(self.code_style_max_line_length)
+        line_length_layout.addWidget(vertical_line_box)
+        line_length_group.setLayout(line_length_layout)
+
+        # Code formatting label
+        autopep8_url = (
+            "<a href='https://github.com/hhatto/autopep8'>Autopep8</a>"
+        )
+        yapf_url = (
+            "<a href='https://github.com/google/yapf'>Yapf</a>"
+        )
+        black_url = (
+            "<a href='https://black.readthedocs.io/en/stable'>Black</a>"
+        )
+        code_fmt_label = QLabel(
+            _("Spyder can use {0}, {1} or {2} to format your code for "
+              "conformance to the {3} convention.").format(
+                  autopep8_url, yapf_url, black_url, pep_url))
+        code_fmt_label.setOpenExternalLinks(True)
+        code_fmt_label.setWordWrap(True)
+
+        # Code formatting providers
+        code_fmt_provider = self.create_combobox(
+            _("Choose the code formatting provider: "),
+            (("autopep8", 'autopep8'),
+             ("black", 'black')),
+            'formatting')
+
+        # Autoformat on save
+        format_on_save_box = newcb(
+            _("Autoformat files on save"),
+            'format_on_save',
+            tip=_("If enabled, autoformatting will take place when "
+                  "saving a file"))
+
+        # Code formatting layout
+        code_fmt_group = QGroupBox(_("Code formatting"))
+        code_fmt_layout = QVBoxLayout()
+        code_fmt_layout.addWidget(code_fmt_label)
+        code_fmt_layout.addWidget(code_fmt_provider)
+        code_fmt_layout.addWidget(format_on_save_box)
+        code_fmt_group.setLayout(code_fmt_layout)
+
+        code_style_fmt_layout = QVBoxLayout()
+        code_style_fmt_layout.addWidget(code_style_group)
+        code_style_fmt_layout.addWidget(code_fmt_group)
+        code_style_fmt_layout.addWidget(line_length_group)
+        self.setLayout(code_style_fmt_layout)
+
+
+class DocstringConfigTab(SpyderPreferencesTab):
+    """Docstring style configuration tab."""
+
+    TITLE = _('Docstring style')
+
+    def __init__(self, parent):
+        super().__init__(parent)
+
+        numpy_url = (
+            "<a href='https://numpydoc.readthedocs.io/en/"
+            "latest/format.html'>Numpy</a>")
+        pep257_url = (
+            "<a href='https://www.python.org/dev/peps/pep-0257/'>PEP 257</a>")
+        docstring_style_codes = _(
+            "<a href='http://www.pydocstyle.org/en/stable"
+            "/error_codes.html'>page</a>")
+        docstring_style_label = QLabel(
+            _("Here you can decide if you want to perform style analysis on "
+              "your docstrings according to the {} or {} conventions. You can "
+              "also decide if you want to show or ignore specific errors, "
+              "according to the codes found on this {}.").format(
+                  numpy_url, pep257_url, docstring_style_codes))
+        docstring_style_label.setOpenExternalLinks(True)
+        docstring_style_label.setWordWrap(True)
+
+        # Docstring style checkbox
+        self.docstring_style_check = self.create_checkbox(
+            _("Enable docstring style linting"),
+            'pydocstyle')
+
+        # Docstring style options
+        docstring_style_convention = self.create_combobox(
+            _("Choose the convention used to lint docstrings: "),
+            (("Numpy", 'numpy'),
+             ("PEP 257", 'pep257'),
+             ("Custom", 'custom')),
+            'pydocstyle/convention')
+        self.docstring_style_select = self.create_lineedit(
+            _("Show the following errors:"),
+            'pydocstyle/select', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Example codes: D413, D414"))
+        self.docstring_style_ignore = self.create_lineedit(
+            _("Ignore the following errors:"),
+            'pydocstyle/ignore', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Example codes: D107, D402"))
+        self.docstring_style_match = self.create_lineedit(
+            _("Only check filenames matching these patterns:"),
+            'pydocstyle/match', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Skip test files: (?!test_).*\\.py"))
+        self.docstring_style_match_dir = self.create_lineedit(
+            _("Only check in directories matching these patterns:"),
+            'pydocstyle/match_dir', alignment=Qt.Horizontal, word_wrap=False,
+            placeholder=_("Skip dot directories: [^\\.].*"))
+
+        # Custom option handling
+        docstring_style_convention.combobox.currentTextChanged.connect(
+                self.setup_docstring_style_convention)
+        current_convention = docstring_style_convention.combobox.currentText()
+        self.setup_docstring_style_convention(current_convention)
+
+        # Docstring style layout
+        docstring_style_g_layout = QGridLayout()
+        docstring_style_g_layout.addWidget(
+            docstring_style_convention.label, 1, 0)
+        docstring_style_g_layout.addWidget(
+            docstring_style_convention.combobox, 1, 1)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_select.label, 2, 0)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_select.textbox, 2, 1)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_ignore.label, 3, 0)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_ignore.textbox, 3, 1)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_match.label, 4, 0)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_match.textbox, 4, 1)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_match_dir.label, 5, 0)
+        docstring_style_g_layout.addWidget(
+            self.docstring_style_match_dir.textbox, 5, 1)
+
+        # Set Docstring style options enabled/disabled
+        docstring_style_g_widget = QWidget()
+        docstring_style_g_widget.setLayout(docstring_style_g_layout)
+        docstring_style_g_widget.setEnabled(self.get_option('pydocstyle'))
+        self.docstring_style_check.toggled.connect(
+            docstring_style_g_widget.setEnabled)
+
+        # Docstring style layout
+        docstring_style_layout = QVBoxLayout()
+        docstring_style_layout.addWidget(docstring_style_label)
+        docstring_style_layout.addWidget(self.docstring_style_check)
+        docstring_style_layout.addWidget(docstring_style_g_widget)
+
+        self.setLayout(docstring_style_layout)
+
+    @Slot(str)
+    def setup_docstring_style_convention(self, text):
+        """Handle convention changes."""
+        if text == 'Custom':
+            self.docstring_style_select.label.setText(
+                _("Show the following errors:"))
+            self.docstring_style_ignore.label.setText(
+                _("Ignore the following errors:"))
+        else:
+            self.docstring_style_select.label.setText(
+                _("Show the following errors in addition "
+                  "to the specified convention:"))
+            self.docstring_style_ignore.label.setText(
+                _("Ignore the following errors in addition "
+                  "to the specified convention:"))
+
+
+class AdvancedConfigTab(SpyderPreferencesTab):
+    """PyLS advanced configuration tab."""
+
+    TITLE = _('Advanced')
+
+    def __init__(self, parent):
+        super().__init__(parent)
+
+        lsp_advanced_group = QGroupBox(_(
+            'Python Language Server configuration'))
+        advanced_label = QLabel(
+            _("<b>Warning</b>: Only modify these values if "
+              "you know what you're doing!"))
+        advanced_label.setWordWrap(True)
+        advanced_label.setAlignment(Qt.AlignJustify)
+
+        # Advanced settings checkbox
+        self.advanced_options_check = self.create_checkbox(
+            _("Enable advanced settings"), 'advanced/enabled')
+
+        # Advanced options
+        self.advanced_module = self.create_lineedit(
+            _("Module for the Python language server: "),
+            'advanced/module', alignment=Qt.Horizontal,
+            word_wrap=False)
+        self.advanced_host = self.create_lineedit(
+            _("IP Address and port to bind the server to: "),
+            'advanced/host', alignment=Qt.Horizontal,
+            word_wrap=False)
+        self.advanced_port = self.create_spinbox(
+            ":", "", 'advanced/port', min_=1, max_=65535, step=1)
+        self.external_server = self.create_checkbox(
+            _("This is an external server"),
+            'advanced/external')
+        self.use_stdio = self.create_checkbox(
+            _("Use stdio pipes to communicate with server"),
+            'advanced/stdio')
+        self.use_stdio.stateChanged.connect(self.disable_tcp)
+        self.external_server.stateChanged.connect(self.disable_stdio)
+
+        # Advanced layout
+        advanced_g_layout = QGridLayout()
+        advanced_g_layout.addWidget(self.advanced_module.label, 1, 0)
+        advanced_g_layout.addWidget(self.advanced_module.textbox, 1, 1)
+        advanced_g_layout.addWidget(self.advanced_host.label, 2, 0)
+
+        advanced_host_port_g_layout = QGridLayout()
+        advanced_host_port_g_layout.addWidget(self.advanced_host.textbox, 1, 0)
+        advanced_host_port_g_layout.addWidget(self.advanced_port.plabel, 1, 1)
+        advanced_host_port_g_layout.addWidget(self.advanced_port.spinbox, 1, 2)
+        advanced_g_layout.addLayout(advanced_host_port_g_layout, 2, 1)
+
+        # External server and stdio options layout
+        advanced_server_layout = QVBoxLayout()
+        advanced_server_layout.addWidget(self.external_server)
+        advanced_server_layout.addWidget(self.use_stdio)
+
+        advanced_options_layout = QVBoxLayout()
+        advanced_options_layout.addLayout(advanced_g_layout)
+        advanced_options_layout.addLayout(advanced_server_layout)
+
+        # Set advanced options enabled/disabled
+        advanced_options_widget = QWidget()
+        advanced_options_widget.setLayout(advanced_options_layout)
+        advanced_options_widget.setEnabled(self.get_option('advanced/enabled'))
+        self.advanced_options_check.toggled.connect(
+            advanced_options_widget.setEnabled)
+        self.advanced_options_check.toggled.connect(
+            self.show_advanced_warning)
+
+        # Advanced options layout
+        advanced_layout = QVBoxLayout()
+        advanced_layout.addWidget(advanced_label)
+        advanced_layout.addWidget(self.advanced_options_check)
+        advanced_layout.addWidget(advanced_options_widget)
+
+        lsp_advanced_group.setLayout(advanced_layout)
+
+        layout = QVBoxLayout()
+        layout.addWidget(lsp_advanced_group)
+        self.setLayout(layout)
+
+    def disable_tcp(self, state):
+        if state == Qt.Checked:
+            self.advanced_host.textbox.setEnabled(False)
+            self.advanced_port.spinbox.setEnabled(False)
+            self.external_server.stateChanged.disconnect()
+            self.external_server.setChecked(False)
+            self.external_server.setEnabled(False)
+        else:
+            self.advanced_host.textbox.setEnabled(True)
+            self.advanced_port.spinbox.setEnabled(True)
+            self.external_server.setChecked(False)
+            self.external_server.setEnabled(True)
+            self.external_server.stateChanged.connect(self.disable_stdio)
+
+    def disable_stdio(self, state):
+        if state == Qt.Checked:
+            self.advanced_host.textbox.setEnabled(True)
+            self.advanced_port.spinbox.setEnabled(True)
+            self.advanced_module.textbox.setEnabled(False)
+            self.use_stdio.stateChanged.disconnect()
+            self.use_stdio.setChecked(False)
+            self.use_stdio.setEnabled(False)
+        else:
+            self.advanced_host.textbox.setEnabled(True)
+            self.advanced_port.spinbox.setEnabled(True)
+            self.advanced_module.textbox.setEnabled(True)
+            self.use_stdio.setChecked(False)
+            self.use_stdio.setEnabled(True)
+            self.use_stdio.stateChanged.connect(self.disable_tcp)
+
+    @Slot(bool)
+    def show_advanced_warning(self, state):
+        """
+        Show a warning when trying to modify the PyLS advanced
+        settings.
+        """
+        # Don't show warning if the option is already enabled.
+        # This avoids showing it when the Preferences dialog
+        # is created.
+        if self.get_option('advanced/enabled'):
+            return
+
+        # Show warning when toggling the button state
+        if state:
+            QMessageBox.warning(
+                self,
+                _("Warning"),
+                _("<b>Modifying these options can break code completion!!</b>"
+                  "<br><br>"
+                  "If that's the case, please reset your Spyder preferences "
+                  "by going to the menu"
+                  "<br><br>"
+                  "<tt>Tools > Reset Spyder to factory defaults</tt>"
+                  "<br><br>"
+                  "instead of reporting a bug."))
+
+
+class OtherLanguagesConfigTab(SpyderPreferencesTab):
+    """LSP server configuration for other languages."""
+
+    TITLE = _('Other languages')
+
+    def __init__(self, parent):
+        super().__init__(parent)
+
+        servers_label = QLabel(
+            _("Spyder uses the <a href=\"{lsp_url}\">Language Server "
+              "Protocol</a> to provide code completion and linting "
+              "for its Editor. Here, you can setup and configure LSP servers "
+              "for languages other than Python, so Spyder can provide such "
+              "features for those languages as well."
+              ).format(lsp_url=LSP_URL))
+        servers_label.setOpenExternalLinks(True)
+        servers_label.setWordWrap(True)
+        servers_label.setAlignment(Qt.AlignJustify)
+
+        # Servers table
+        table_group = QGroupBox(_('Available servers:'))
+        self.table = LSPServerTable(self, text_color=ima.MAIN_FG_COLOR)
+        self.table.setMaximumHeight(150)
+        table_layout = QVBoxLayout()
+        table_layout.addWidget(self.table)
+        table_group.setLayout(table_layout)
+
+        # Buttons
+        self.reset_btn = QPushButton(_("Reset to default values"))
+        self.new_btn = QPushButton(_("Set up a new server"))
+        self.delete_btn = QPushButton(_("Delete currently selected server"))
+        self.delete_btn.setEnabled(False)
+
+        # Slots connected to buttons
+        self.new_btn.clicked.connect(self.create_new_server)
+        self.reset_btn.clicked.connect(self.reset_to_default)
+        self.delete_btn.clicked.connect(self.delete_server)
+
+        # Buttons layout
+        btns = [self.new_btn, self.delete_btn, self.reset_btn]
+        buttons_layout = QGridLayout()
+        for i, btn in enumerate(btns):
+            buttons_layout.addWidget(btn, i, 1)
+        buttons_layout.setColumnStretch(0, 1)
+        buttons_layout.setColumnStretch(1, 2)
+        buttons_layout.setColumnStretch(2, 1)
+
+        # Combined layout
+        servers_layout = QVBoxLayout()
+        servers_layout.addSpacing(-10)
+        servers_layout.addWidget(servers_label)
+        servers_layout.addWidget(table_group)
+        servers_layout.addSpacing(10)
+        servers_layout.addLayout(buttons_layout)
+
+        self.setLayout(servers_layout)
+
+    def create_new_server(self):
+        self.table.show_editor(new_server=True)
+
+    def delete_server(self):
+        idx = self.table.currentIndex().row()
+        self.table.delete_server(idx)
+        self.set_modified(True)
+        self.delete_btn.setEnabled(False)
+
+    def reset_to_default(self):
+        # TODO: Improve this logic when more languages are added on the
+        # configuration
+        # Remove all non-Python servers
+        for language in SUPPORTED_LANGUAGES:
+            language = language.lower()
+            conf = self.get_option(language, default=None)
+            if conf is not None:
+                self.table.delete_server_by_lang(language)
+
+    def apply_settings(self):
+        return self.table.save_servers()
+
+# LSP provider tabs
+TABS = [
+    LintingConfigPage,
+    IntrospectionConfigTab,
+    FormattingStyleConfigTab,
+    DocstringConfigTab,
+    AdvancedConfigTab,
+    OtherLanguagesConfigTab
+]
diff --git a/spyder/plugins/completion/languageserver/decorators.py b/spyder/plugins/completion/providers/languageserver/decorators.py
similarity index 95%
rename from spyder/plugins/completion/languageserver/decorators.py
rename to spyder/plugins/completion/providers/languageserver/decorators.py
index 689cdd9d302..0cc0e010fa0 100644
--- a/spyder/plugins/completion/languageserver/decorators.py
+++ b/spyder/plugins/completion/providers/languageserver/decorators.py
@@ -7,7 +7,7 @@
 """Spyder Language Server Protocol Client auxiliar decorators."""
 
 import functools
-from spyder.plugins.completion.languageserver.transport import MessageKind
+from spyder.plugins.completion.providers.languageserver.transport import MessageKind
 
 
 def send_request(req=None, method=None):
diff --git a/spyder/plugins/completion/languageserver/plugin.py b/spyder/plugins/completion/providers/languageserver/provider.py
similarity index 82%
rename from spyder/plugins/completion/languageserver/plugin.py
rename to spyder/plugins/completion/providers/languageserver/provider.py
index f943764f149..907ec4e2aad 100644
--- a/spyder/plugins/completion/languageserver/plugin.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -26,29 +26,81 @@
 from spyder.config.lsp import PYTHON_CONFIG
 from spyder.config.manager import CONF
 from spyder.utils.misc import check_connection_port
-from spyder.plugins.completion.manager.api import (LSP_LANGUAGES,
-                                                   SpyderCompletionPlugin)
-from spyder.plugins.completion.languageserver.client import LSPClient
-from spyder.plugins.completion.languageserver.confpage import (
-    LanguageServerConfigPage)
-from spyder.plugins.completion.languageserver.widgets.status import (
+from spyder.plugins.completion.api import (SUPPORTED_LANGUAGES,
+                                           SpyderCompletionProvider,
+                                           WorkspaceUpdateKind)
+from spyder.plugins.completion.providers.languageserver.client import LSPClient
+from spyder.plugins.completion.providers.languageserver.conftabs import TABS
+# TODO: Define config page behaviour
+# from spyder.plugins.completion.providers.languageserver.confpage import (
+#     LanguageServerConfigPage)
+# TODO: Define status widget behaviour
+from spyder.plugins.completion.providers.languageserver.widgets.status import (
     ClientStatus, LSPStatusWidget)
-from spyder.widgets.helperwidgets import MessageCheckBox
+# from spyder.widgets.helperwidgets import MessageCheckBox
+from spyder.plugins.completion.providers.languageserver.widgets.message_box import (
+    ServerDisabledMessageBox
+)
+from spyder.utils.introspection.module_completion import PREFERRED_MODULES
 
+# Modules to be preloaded for Rope and Jedi
+PRELOAD_MDOULES = ', '.join(PREFERRED_MODULES)
 
 logger = logging.getLogger(__name__)
 
 
-class LanguageServerPlugin(SpyderCompletionPlugin):
+class LanguageServerProvider(SpyderCompletionProvider):
     """Language Server Protocol manager."""
-    CONF_SECTION = 'lsp-server'
-    CONF_FILE = False
-
     COMPLETION_CLIENT_NAME = 'lsp'
+    DEFAULT_ORDER = 1
+    CONF_DEFAULTS = [
+        ('enable_hover_hints', True),
+        ('show_lsp_down_warning', True),
+        ('code_completion', True),
+        ('code_snippets', True),
+        ('jedi_definition', True),
+        ('jedi_definition/follow_imports', True),
+        ('jedi_signature_help', True),
+        ('preload_modules', PRELOAD_MDOULES),
+        ('pyflakes', True),
+        ('mccabe', False),
+        ('formatting', 'autopep8'),
+        ('format_on_save', False),
+        ('pycodestyle', False),
+        ('pycodestyle/filename', ''),
+        ('pycodestyle/exclude', ''),
+        ('pycodestyle/select', ''),
+        ('pycodestyle/ignore', ''),
+        ('pycodestyle/max_line_length', 79),
+        ('pydocstyle', False),
+        ('pydocstyle/convention', 'numpy'),
+        ('pydocstyle/select', ''),
+        ('pydocstyle/ignore', ''),
+        ('pydocstyle/match', '(?!test_).*\\.py'),
+        ('pydocstyle/match_dir', '[^\\.].*'),
+        ('advanced/enabled', False),
+        ('advanced/module', 'pyls'),
+        ('advanced/host', '127.0.0.1'),
+        ('advanced/port', 2087),
+        ('advanced/external', False),
+        ('advanced/stdio', False)
+    ]
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+    CONF_TABS = TABS
+
     STOPPED = 'stopped'
     RUNNING = 'running'
     LOCALHOST = ['127.0.0.1', 'localhost']
-    CONFIGWIDGET_CLASS = LanguageServerConfigPage
+    # TODO: Finish configuration tabs migration
+    # CONFIGWIDGET_CLASS = LanguageServerConfigPage
     MAX_RESTART_ATTEMPTS = 5
     TIME_BETWEEN_RESTARTS = 10000  # ms
     TIME_HEARTBEAT = 3000  # ms
@@ -77,8 +129,8 @@ class LanguageServerPlugin(SpyderCompletionPlugin):
     `title` indicates how the error data should customize the report dialog.
     """
 
-    def __init__(self, parent):
-        SpyderCompletionPlugin.__init__(self, parent)
+    def __init__(self, parent, config):
+        SpyderCompletionProvider.__init__(self, parent, config)
 
         self.clients = {}
         self.clients_restart_count = {}
@@ -88,18 +140,14 @@ def __init__(self, parent):
         self.clients_statusbar = {}
         self.requests = set({})
         self.register_queue = {}
-        self.update_configuration()
+        self.update_lsp_configuration()
         self.show_no_external_server_warning = True
+        self.current_project_path = None
 
         # Status bar widget
-        if parent is not None:
-            self.status_widget = LSPStatusWidget(parent=None, plugin=self)
-            statusbar = self.main.statusbar
-            statusbar.add_status_widget(self.status_widget)
-
-        # TODO: Move to register in the new API
-        self.sig_exception_occurred.connect(
-            self.main.console.handle_exception)
+        self.STATUS_BARS = {
+            'lsp_statusbar': self.create_statusbar
+        }
 
     def __del__(self):
         """Stop all heartbeats"""
@@ -110,7 +158,7 @@ def __del__(self):
                 pass
 
     # --- Status bar widget handling
-    def restart_lsp(self, language, force=False):
+    def restart_lsp(self, language: str, force=False):
         """Restart language server on failure."""
         client_config = {
             'status': self.STOPPED,
@@ -149,6 +197,9 @@ def restart_lsp(self, language, force=False):
                 self.clients_hearbeat[language] = None
                 self.report_lsp_down(language)
 
+    def create_statusbar(self, parent):
+        return LSPStatusWidget(parent, self)
+
     def check_restart(self, client, language):
         """
         Check if a server restart was successful in order to stop
@@ -186,7 +237,9 @@ def update_status(self, language, status):
         Update status for the current file.
         """
         self.clients_statusbar[language] = status
-        self.status_widget.update_status(language, status)
+        self.sig_update_statusbar.emit(
+            'lsp_statusbar', 'update_status', (language, status), {})
+        # self.status_widget.update_status(language, status)
 
     def on_initialize(self, options, language):
         """
@@ -224,7 +277,10 @@ def handle_lsp_down(self, language):
             self.clients_restart_timers[language] = timer
             timer.start()
 
-    # --- Other methods
+    # ------------------ SpyderCompletionProvider methods ---------------------
+    def get_name(self):
+        return _('Language Server Protocol (LSP)')
+
     def register_file(self, language, filename, codeeditor):
         if language in self.clients:
             language_client = self.clients[language]['instance']
@@ -239,9 +295,9 @@ def get_languages(self):
         clients for.
         """
         languages = ['python']
-        all_options = CONF.options(self.CONF_SECTION)
+        all_options = self.config
         for option in all_options:
-            if option in [l.lower() for l in LSP_LANGUAGES]:
+            if option in [l.lower() for l in SUPPORTED_LANGUAGES]:
                 languages.append(option)
         return languages
 
@@ -259,11 +315,7 @@ def get_root_path(self, language):
         This can be the current project path or the output of
         getcwd_or_home (except for Python, see below).
         """
-        path = None
-
-        # Get path of the current project
-        if self.main and self.main.projects:
-            path = self.main.projects.get_active_project_path()
+        path = self.current_project_path
 
         if not path:
             # We can't use getcwd_or_home for LSP servers because if it
@@ -279,7 +331,7 @@ def get_root_path(self, language):
         return path
 
     @Slot()
-    def project_path_update(self, project_path, update_kind):
+    def project_path_update(self, project_path, update_kind, projects):
         """
         Send a didChangeWorkspaceFolders request to each LSP server
         when the project path changes so they can update their
@@ -288,6 +340,9 @@ def project_path_update(self, project_path, update_kind):
         If the server doesn't support workspace updates, restart the
         client with the new root path.
         """
+        if update_kind == WorkspaceUpdateKind.ADDITION:
+            self.current_project_path = project_path
+
         for language in self.clients:
             language_client = self.clients[language]
             if language_client['status'] == self.RUNNING:
@@ -296,7 +351,7 @@ def project_path_update(self, project_path, update_kind):
                         instance.support_workspace_update):
                     instance.send_workspace_folders_change({
                         'folder': project_path,
-                        'instance': self.main.projects,
+                        'instance': projects,
                         'kind': update_kind
                     })
                 else:
@@ -304,13 +359,10 @@ def project_path_update(self, project_path, update_kind):
                         "{0}: LSP does not support multiple workspaces, "
                         "restarting client!".format(instance.language)
                     )
-                    self.main.projects.stop_workspace_services()
-                    self.main.editor.stop_completion_services(language)
-                    self.main.outlineexplorer.stop_symbol_services(language)
                     folder = self.get_root_path(language)
                     instance.folder = folder
-                    self.close_client(language)
-                    self.start_client(language)
+                    self.stop_provider(language)
+                    self.start_provider(language)
 
     @Slot(str)
     def report_server_error(self, error):
@@ -354,12 +406,10 @@ def report_no_external_server(self, host, port, language):
             + os_message
         )
 
-        QMessageBox.warning(
-            self.main,
-            _("Warning"),
-            warn_str
-        )
+        def wrap_message_box(parent):
+            return QMessageBox.warning(parent, _("Warning"), warn_str)
 
+        self.sig_show_widget.emit(wrap_message_box)
         self.show_no_external_server_warning = False
 
     @Slot(str)
@@ -392,22 +442,10 @@ def report_lsp_down(self, language):
             _("Do you want to restart Spyder now?")
         )
 
-        box = MessageCheckBox(icon=QMessageBox.Warning, parent=self.main)
-        box.setWindowTitle(_("Warning"))
-        box.set_checkbox_text(_("Don't show again"))
-        box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
-        box.setDefaultButton(QMessageBox.No)
-        box.set_checked(False)
-        box.set_check_visible(True)
-        box.setText(warn_str)
-        answer = box.exec_()
-
-        self.set_option('show_lsp_down_warning', not box.is_checked())
+        wrapper = ServerDisabledMessageBox.instance(warn_str, self.set_option)
+        self.sig_show_widget.emit(wrapper)
 
-        if answer == QMessageBox.Yes:
-            self.main.restart()
-
-    def start_client(self, language):
+    def start_provider(self, language):
         """Start an LSP client for a given language."""
         # To keep track if the client was started.
         started = False
@@ -471,35 +509,18 @@ def start_client(self, language):
 
     def register_client_instance(self, instance):
         """Register signals emitted by a client instance."""
-        if self.main:
-            self.main.sig_pythonpath_changed.connect(self.update_syspath)
-            self.main.sig_main_interpreter_changed.connect(
-                functools.partial(self.update_configuration, python_only=True))
-            instance.sig_went_down.connect(self.handle_lsp_down)
-            instance.sig_initialize.connect(self.on_initialize)
-
-            if self.main.projects:
-                instance.sig_initialize.connect(
-                    lambda settings, language:
-                    self.main.projects.start_workspace_services())
-            if self.main.editor:
-                instance.sig_initialize.connect(
-                    self.main.editor.register_completion_capabilities)
-                self.main.editor.sig_editor_focus_changed.connect(
-                    self.status_widget.update_status)
-            if self.main.outlineexplorer:
-                instance.sig_initialize.connect(
-                    self.main.outlineexplorer.start_symbol_services)
-            if self.main.console:
-                instance.sig_server_error.connect(self.report_server_error)
+        instance.sig_went_down.connect(self.handle_lsp_down)
+        instance.sig_initialize.connect(self.on_initialize)
+        instance.sig_server_error.connect(self.report_server_error)
+        instance.sig_initialize.connect(self.sig_language_client_available)
 
     def shutdown(self):
         logger.info("Shutting down LSP manager...")
         for language in self.clients:
-            self.close_client(language)
+            self.stop_provider(language)
 
     @Slot(object, object)
-    def update_syspath(self, path_dict, new_path_dict):
+    def python_path_update(self, path_dict, new_path_dict):
         """
         Update server configuration after a change in Spyder's Python
         path.
@@ -517,9 +538,24 @@ def update_syspath(self, path_dict, new_path_dict):
 
         if update:
             logger.debug("Update server's sys.path")
-            self.update_configuration(python_only=True)
+            self.update_lsp_configuration(python_only=True)
+
+    @Slot()
+    def main_interpreter_changed(self):
+        self.update_lsp_configuration(python_only=True)
+
+    def editor_focus_changed(self, filename: str, language: str):
+        # TODO: Update status widget here.
+        #     self.main.editor.sig_editor_focus_changed.connect(
+        #         self.status_widget.update_status)
+        self.sig_update_statusbar.emit(
+            'lsp_statusbar', 'set_current_language', (language,), {})
+
+    def update_configuration(self, config):
+        self.config = config
+        self.update_lsp_configuration()
 
-    def update_configuration(self, python_only=False):
+    def update_lsp_configuration(self, python_only=False):
         """
         Update server configuration after changes done by the user
         through Spyder's Preferences.
@@ -565,25 +601,21 @@ def update_configuration(self, python_only=False):
 
     def restart_client(self, language, config):
         """Restart a client."""
-        self.main.editor.stop_completion_services(language)
-        self.main.projects.stop_workspace_services()
-        self.main.outlineexplorer.stop_symbol_services(language)
-        self.close_client(language)
+        self.stop_provider(language)
         self.clients[language] = config
-        self.start_client(language)
+        self.start_provider(language)
 
     def update_client_status(self, active_set):
         for language in self.clients:
             if language not in active_set:
-                self.close_client(language)
+                self.stop_provider(language)
 
-    def close_client(self, language):
+    def stop_provider(self, language):
         if language in self.clients:
             language_client = self.clients[language]
             if language_client['status'] == self.RUNNING:
                 logger.info("Stopping LSP client for {}...".format(language))
-                # language_client['instance'].shutdown()
-                # language_client['instance'].exit()
+                language_client['instance'].disconnect()
                 language_client['instance'].stop()
             language_client['status'] = self.STOPPED
 
diff --git a/spyder/plugins/completion/languageserver/providers/__init__.py b/spyder/plugins/completion/providers/languageserver/providers/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/providers/__init__.py
rename to spyder/plugins/completion/providers/languageserver/providers/__init__.py
diff --git a/spyder/plugins/completion/languageserver/providers/client.py b/spyder/plugins/completion/providers/languageserver/providers/client.py
similarity index 72%
rename from spyder/plugins/completion/languageserver/providers/client.py
rename to spyder/plugins/completion/providers/languageserver/providers/client.py
index 5710e51ed58..571a9f9f3a2 100644
--- a/spyder/plugins/completion/languageserver/providers/client.py
+++ b/spyder/plugins/completion/providers/languageserver/providers/client.py
@@ -8,15 +8,15 @@
 
 import logging
 
-from spyder.plugins.completion.manager.api import LSPRequestTypes
-from spyder.plugins.completion.languageserver.decorators import (
+from spyder.plugins.completion.api import CompletionRequestTypes
+from spyder.plugins.completion.providers.languageserver.decorators import (
     handles, send_response)
 
 logger = logging.getLogger(__name__)
 
 
 class ClientProvider:
-    @handles(LSPRequestTypes.CLIENT_REGISTER_CAPABILITY)
+    @handles(CompletionRequestTypes.CLIENT_REGISTER_CAPABILITY)
     @send_response
     def handle_register_capability(self, params):
         """TODO: Handle the glob patterns of the files to watch."""
diff --git a/spyder/plugins/completion/languageserver/providers/document.py b/spyder/plugins/completion/providers/languageserver/providers/document.py
similarity index 80%
rename from spyder/plugins/completion/languageserver/providers/document.py
rename to spyder/plugins/completion/providers/languageserver/providers/document.py
index f1627cd8bd0..eac7ddb5f88 100644
--- a/spyder/plugins/completion/languageserver/providers/document.py
+++ b/spyder/plugins/completion/providers/languageserver/providers/document.py
@@ -10,12 +10,12 @@
 import logging
 
 # Local imports
-from spyder.plugins.completion.manager.api import (
-    LSPRequestTypes, InsertTextFormat, CompletionItemKind,
+from spyder.plugins.completion.api import (
+    CompletionRequestTypes, InsertTextFormat, CompletionItemKind,
     ClientConstants)
-from spyder.plugins.completion.languageserver.providers.utils import (
+from spyder.plugins.completion.providers.languageserver.providers.utils import (
     path_as_uri, process_uri, snake_to_camel)
-from spyder.plugins.completion.languageserver.decorators import (
+from spyder.plugins.completion.providers.languageserver.decorators import (
     handles, send_request, send_notification)
 
 
@@ -31,7 +31,7 @@ def register_file(self, filename, codeeditor):
             self.watched_files[filename] = []
         self.watched_files[filename].append(codeeditor)
 
-    @handles(LSPRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS)
+    @handles(CompletionRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS)
     def process_document_diagnostics(self, response, *args):
         uri = response['uri']
         diagnostics = response['diagnostics']
@@ -39,12 +39,12 @@ def process_document_diagnostics(self, response, *args):
             callbacks = self.watched_files[uri]
             for callback in callbacks:
                 callback.handle_response(
-                    LSPRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS,
+                    CompletionRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS,
                     {'params': diagnostics})
         else:
             logger.debug("Received diagnostics for file not open: " + uri)
 
-    @send_notification(method=LSPRequestTypes.DOCUMENT_DID_CHANGE)
+    @send_notification(method=CompletionRequestTypes.DOCUMENT_DID_CHANGE)
     def document_changed(self, params):
         params = {
             'textDocument': {
@@ -57,7 +57,7 @@ def document_changed(self, params):
         }
         return params
 
-    @send_notification(method=LSPRequestTypes.DOCUMENT_DID_OPEN)
+    @send_notification(method=CompletionRequestTypes.DOCUMENT_DID_OPEN)
     def document_open(self, editor_params):
         uri = path_as_uri(editor_params['file'])
         if uri not in self.watched_files:
@@ -74,7 +74,7 @@ def document_open(self, editor_params):
 
         return params
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_COMPLETION)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_COMPLETION)
     def document_completion_request(self, params):
         params = {
             'textDocument': {
@@ -88,7 +88,7 @@ def document_completion_request(self, params):
 
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_COMPLETION)
+    @handles(CompletionRequestTypes.DOCUMENT_COMPLETION)
     def process_document_completion(self, response, req_id):
         if isinstance(response, dict):
             response = response['items']
@@ -106,9 +106,9 @@ def process_document_completion(self, response, req_id):
 
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_COMPLETION, {'params': response})
+                CompletionRequestTypes.DOCUMENT_COMPLETION, {'params': response})
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_SIGNATURE)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_SIGNATURE)
     def signature_help_request(self, params):
         params = {
             'textDocument': {
@@ -122,7 +122,7 @@ def signature_help_request(self, params):
 
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_SIGNATURE)
+    @handles(CompletionRequestTypes.DOCUMENT_SIGNATURE)
     def process_signature_completion(self, response, req_id):
         if len(response['signatures']) > 0:
             response['signatures'] = response['signatures'][
@@ -132,10 +132,10 @@ def process_signature_completion(self, response, req_id):
             response = None
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_SIGNATURE,
+                CompletionRequestTypes.DOCUMENT_SIGNATURE,
                 {'params': response})
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_HOVER)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_HOVER)
     def hover_request(self, params):
         params = {
             'textDocument': {
@@ -149,7 +149,7 @@ def hover_request(self, params):
 
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_HOVER)
+    @handles(CompletionRequestTypes.DOCUMENT_HOVER)
     def process_hover_result(self, result, req_id):
         contents = result['contents']
         if isinstance(contents, dict):
@@ -165,10 +165,10 @@ def process_hover_result(self, result, req_id):
             contents = '\n\n'.join(text)
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_HOVER,
+                CompletionRequestTypes.DOCUMENT_HOVER,
                 {'params': contents})
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_SYMBOL)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_SYMBOL)
     def document_symbol_request(self, params):
         params = {
             'textDocument': {
@@ -177,13 +177,13 @@ def document_symbol_request(self, params):
         }
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_SYMBOL)
+    @handles(CompletionRequestTypes.DOCUMENT_SYMBOL)
     def process_document_symbol_request(self, result, req_id):
         if req_id in self.req_reply:
-            self.req_reply[req_id](LSPRequestTypes.DOCUMENT_SYMBOL,
+            self.req_reply[req_id](CompletionRequestTypes.DOCUMENT_SYMBOL,
                                    {'params': result})
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_DEFINITION)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_DEFINITION)
     def go_to_definition_request(self, params):
         params = {
             'textDocument': {
@@ -197,7 +197,7 @@ def go_to_definition_request(self, params):
 
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_DEFINITION)
+    @handles(CompletionRequestTypes.DOCUMENT_DEFINITION)
     def process_go_to_definition(self, result, req_id):
         if isinstance(result, list):
             if len(result) > 0:
@@ -209,10 +209,10 @@ def process_go_to_definition(self, result, req_id):
             result['file'] = process_uri(result['uri'])
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_DEFINITION,
+                CompletionRequestTypes.DOCUMENT_DEFINITION,
                 {'params': result})
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_FOLDING_RANGE)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_FOLDING_RANGE)
     def folding_range_request(self, params):
         params = {
             'textDocument': {
@@ -221,7 +221,7 @@ def folding_range_request(self, params):
         }
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_FOLDING_RANGE)
+    @handles(CompletionRequestTypes.DOCUMENT_FOLDING_RANGE)
     def process_folding_range(self, result, req_id):
         results = []
         for folding_range in result:
@@ -230,10 +230,10 @@ def process_folding_range(self, result, req_id):
             results.append((start_line, end_line))
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_FOLDING_RANGE,
+                CompletionRequestTypes.DOCUMENT_FOLDING_RANGE,
                 {'params': results})
 
-    @send_notification(method=LSPRequestTypes.DOCUMENT_WILL_SAVE)
+    @send_notification(method=CompletionRequestTypes.DOCUMENT_WILL_SAVE)
     def document_will_save_notification(self, params):
         params = {
             'textDocument': {
@@ -243,7 +243,7 @@ def document_will_save_notification(self, params):
         }
         return params
 
-    @send_notification(method=LSPRequestTypes.DOCUMENT_DID_SAVE)
+    @send_notification(method=CompletionRequestTypes.DOCUMENT_DID_SAVE)
     def document_did_save_notification(self, params):
         """
         Handle the textDocument/didSave message received from an LSP server.
@@ -260,7 +260,7 @@ def document_did_save_notification(self, params):
             params['text'] = text
         return params
 
-    @send_notification(method=LSPRequestTypes.DOCUMENT_DID_CLOSE)
+    @send_notification(method=CompletionRequestTypes.DOCUMENT_DID_CLOSE)
     def document_did_close(self, params):
         codeeditor = params['codeeditor']
         filename = path_as_uri(params['file'])
@@ -289,7 +289,7 @@ def document_did_close(self, params):
 
         return params
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_FORMATTING)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_FORMATTING)
     def document_formatting_request(self, params):
         options = params['options']
         options = {
@@ -305,14 +305,14 @@ def document_formatting_request(self, params):
         }
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_FORMATTING)
+    @handles(CompletionRequestTypes.DOCUMENT_FORMATTING)
     def process_document_formatting(self, result, req_id):
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_FORMATTING,
+                CompletionRequestTypes.DOCUMENT_FORMATTING,
                 {'params': result})
 
-    @send_request(method=LSPRequestTypes.DOCUMENT_RANGE_FORMATTING)
+    @send_request(method=CompletionRequestTypes.DOCUMENT_RANGE_FORMATTING)
     def document_range_formatting_request(self, params):
         options = params['options']
         options = {
@@ -328,9 +328,9 @@ def document_range_formatting_request(self, params):
         }
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_RANGE_FORMATTING)
+    @handles(CompletionRequestTypes.DOCUMENT_RANGE_FORMATTING)
     def process_document_range_formatting(self, result, req_id):
         if req_id in self.req_reply:
             self.req_reply[req_id](
-                LSPRequestTypes.DOCUMENT_RANGE_FORMATTING,
+                CompletionRequestTypes.DOCUMENT_RANGE_FORMATTING,
                 {'params': result})
diff --git a/spyder/plugins/completion/languageserver/providers/utils.py b/spyder/plugins/completion/providers/languageserver/providers/utils.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/providers/utils.py
rename to spyder/plugins/completion/providers/languageserver/providers/utils.py
diff --git a/spyder/plugins/completion/languageserver/providers/window.py b/spyder/plugins/completion/providers/languageserver/providers/window.py
similarity index 71%
rename from spyder/plugins/completion/languageserver/providers/window.py
rename to spyder/plugins/completion/providers/languageserver/providers/window.py
index c0b79c7f345..4d90c809a40 100644
--- a/spyder/plugins/completion/languageserver/providers/window.py
+++ b/spyder/plugins/completion/providers/languageserver/providers/window.py
@@ -8,19 +8,20 @@
 
 import logging
 
-from spyder.plugins.completion.manager.api import LSPRequestTypes
-from spyder.plugins.completion.languageserver.decorators import handles
+from spyder.plugins.completion.api import CompletionRequestTypes
+from spyder.plugins.completion.providers.languageserver.decorators import (
+    handles)
 
 logger = logging.getLogger(__name__)
 
 
 class WindowProvider:
-    @handles(LSPRequestTypes.WINDOW_SHOW_MESSAGE)
+    @handles(CompletionRequestTypes.WINDOW_SHOW_MESSAGE)
     def process_show_message(self, response, *args):
         """Handle window/showMessage notifications from LSP server."""
         logger.debug("Received showMessage: %r" % response)
 
-    @handles(LSPRequestTypes.WINDOW_LOG_MESSAGE)
+    @handles(CompletionRequestTypes.WINDOW_LOG_MESSAGE)
     def process_log_message(self, response, *args):
         """Handle window/logMessage notifications from LSP server."""
         logger.debug("Received logMessage: %r" % response)
diff --git a/spyder/plugins/completion/languageserver/providers/workspace.py b/spyder/plugins/completion/providers/languageserver/providers/workspace.py
similarity index 85%
rename from spyder/plugins/completion/languageserver/providers/workspace.py
rename to spyder/plugins/completion/providers/languageserver/providers/workspace.py
index b820c06cf38..92bc2afbeab 100644
--- a/spyder/plugins/completion/languageserver/providers/workspace.py
+++ b/spyder/plugins/completion/providers/languageserver/providers/workspace.py
@@ -8,18 +8,18 @@
 
 import logging
 
-from spyder.plugins.completion.languageserver.providers.utils import (
+from spyder.plugins.completion.providers.languageserver.providers.utils import (
     path_as_uri, process_uri, match_path_to_folder)
-from spyder.plugins.completion.manager.api import (
-    LSPRequestTypes, ClientConstants, WorkspaceUpdateKind)
-from spyder.plugins.completion.languageserver.decorators import (
+from spyder.plugins.completion.api import (
+    CompletionRequestTypes, ClientConstants, WorkspaceUpdateKind)
+from spyder.plugins.completion.providers.languageserver.decorators import (
     handles, send_request, send_response, send_notification)
 
 logger = logging.getLogger(__name__)
 
 
 class WorkspaceProvider:
-    @send_notification(method=LSPRequestTypes.WORKSPACE_CONFIGURATION_CHANGE)
+    @send_notification(method=CompletionRequestTypes.WORKSPACE_CONFIGURATION_CHANGE)
     def send_configurations(self, configurations, *args):
         self.configurations = configurations
         params = {
@@ -28,7 +28,7 @@ def send_configurations(self, configurations, *args):
         return params
 
     @send_response
-    @handles(LSPRequestTypes.WORKSPACE_FOLDERS)
+    @handles(CompletionRequestTypes.WORKSPACE_FOLDERS)
     def send_workspace_folders(self, response):
         workspace_folders = []
         for folder_name in self.watched_folders:
@@ -39,7 +39,7 @@ def send_workspace_folders(self, response):
             })
         return workspace_folders
 
-    @send_notification(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE)
+    @send_notification(method=CompletionRequestTypes.WORKSPACE_FOLDERS_CHANGE)
     def send_workspace_folders_change(self, params):
         folder = params['folder']
         workspace_watcher = params['instance']
@@ -83,12 +83,12 @@ def send_workspace_folders_change(self, params):
         return request_params
 
     @send_response
-    @handles(LSPRequestTypes.WORKSPACE_CONFIGURATION)
+    @handles(CompletionRequestTypes.WORKSPACE_CONFIGURATION)
     def send_workspace_configuration(self, params):
         logger.debug(params)
         return self.configurations
 
-    @send_notification(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE)
+    @send_notification(method=CompletionRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE)
     def send_watched_files_change(self, params):
         changes = []
         entries = params.get('params', [])
@@ -102,14 +102,14 @@ def send_watched_files_change(self, params):
         }
         return params
 
-    @send_request(method=LSPRequestTypes.WORKSPACE_SYMBOL)
+    @send_request(method=CompletionRequestTypes.WORKSPACE_SYMBOL)
     def send_symbol_request(self, params):
         params = {
             'query': params['query']
         }
         return params
 
-    @handles(LSPRequestTypes.WORKSPACE_SYMBOL)
+    @handles(CompletionRequestTypes.WORKSPACE_SYMBOL)
     def handle_symbol_response(self, response):
         folders = list(self.watched_folders.keys())
         assigned_symbols = {folder: [] for folder in self.watched_folders}
@@ -124,10 +124,10 @@ def handle_symbol_response(self, response):
             workspace_edits = assigned_symbols[workspace]
             workspace_instance = self.watched_folders[workspace]['instance']
             workspace_instance.handle_response(
-                LSPRequestTypes.WORKSPACE_SYMBOL,
+                CompletionRequestTypes.WORKSPACE_SYMBOL,
                 {'params': workspace_edits})
 
-    @send_request(method=LSPRequestTypes.WORKSPACE_EXECUTE_COMMAND)
+    @send_request(method=CompletionRequestTypes.WORKSPACE_EXECUTE_COMMAND)
     def send_execute_command(self, params):
         # It is not clear how this call is invoked
         params = {
@@ -136,7 +136,7 @@ def send_execute_command(self, params):
         }
         return params
 
-    @send_response(method=LSPRequestTypes.WORKSPACE_APPLY_EDIT)
+    @send_response(method=CompletionRequestTypes.WORKSPACE_APPLY_EDIT)
     def send_edit_response(self, edits):
         params = {
             'applied': edits['applied']
@@ -145,7 +145,7 @@ def send_edit_response(self, edits):
             params['failureReason'] = edits['error']
         return params
 
-    @handles(LSPRequestTypes.WORKSPACE_APPLY_EDIT)
+    @handles(CompletionRequestTypes.WORKSPACE_APPLY_EDIT)
     def apply_edit(self, response):
         logger.debug("Editing: {0}".format(response['label']))
         response = response['edit']
@@ -184,6 +184,6 @@ def apply_edit(self, response):
             workspace_edits = assigned_files[workspace]
             workspace_instance = self.watched_folders[workspace]['instance']
             workspace_instance.handle_response(
-                LSPRequestTypes.WORKSPACE_APPLY_EDIT,
+                CompletionRequestTypes.WORKSPACE_APPLY_EDIT,
                 {'params': {'edits': workspace_edits,
                             'language': self.language}})
diff --git a/spyder/plugins/completion/languageserver/transport/stdio/__init__.py b/spyder/plugins/completion/providers/languageserver/tests/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/transport/stdio/__init__.py
rename to spyder/plugins/completion/providers/languageserver/tests/__init__.py
diff --git a/spyder/plugins/completion/languageserver/tests/conftest.py b/spyder/plugins/completion/providers/languageserver/tests/conftest.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/tests/conftest.py
rename to spyder/plugins/completion/providers/languageserver/tests/conftest.py
diff --git a/spyder/plugins/completion/languageserver/tests/test_client.py b/spyder/plugins/completion/providers/languageserver/tests/test_client.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/tests/test_client.py
rename to spyder/plugins/completion/providers/languageserver/tests/test_client.py
diff --git a/spyder/plugins/completion/languageserver/tests/test_lsp_config_dialog.py b/spyder/plugins/completion/providers/languageserver/tests/test_lsp_config_dialog.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/tests/test_lsp_config_dialog.py
rename to spyder/plugins/completion/providers/languageserver/tests/test_lsp_config_dialog.py
diff --git a/spyder/plugins/completion/languageserver/transport/__init__.py b/spyder/plugins/completion/providers/languageserver/transport/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/transport/__init__.py
rename to spyder/plugins/completion/providers/languageserver/transport/__init__.py
diff --git a/spyder/plugins/completion/languageserver/transport/tcp/__init__.py b/spyder/plugins/completion/providers/languageserver/transport/common/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/transport/tcp/__init__.py
rename to spyder/plugins/completion/providers/languageserver/transport/common/__init__.py
diff --git a/spyder/plugins/completion/languageserver/transport/common/consumer.py b/spyder/plugins/completion/providers/languageserver/transport/common/consumer.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/transport/common/consumer.py
rename to spyder/plugins/completion/providers/languageserver/transport/common/consumer.py
diff --git a/spyder/plugins/completion/languageserver/transport/common/producer.py b/spyder/plugins/completion/providers/languageserver/transport/common/producer.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/transport/common/producer.py
rename to spyder/plugins/completion/providers/languageserver/transport/common/producer.py
diff --git a/spyder/plugins/completion/languageserver/transport/main.py b/spyder/plugins/completion/providers/languageserver/transport/main.py
similarity index 96%
rename from spyder/plugins/completion/languageserver/transport/main.py
rename to spyder/plugins/completion/providers/languageserver/transport/main.py
index f5a8a5650f8..cf3c9ada30b 100644
--- a/spyder/plugins/completion/languageserver/transport/main.py
+++ b/spyder/plugins/completion/providers/languageserver/transport/main.py
@@ -23,9 +23,9 @@
 from functools import partial
 
 # Local imports
-from spyder.plugins.completion.languageserver.transport.tcp.producer import (
+from spyder.plugins.completion.providers.languageserver.transport.tcp.producer import (
     TCPLanguageServerClient)
-from spyder.plugins.completion.languageserver.transport.stdio.producer import (
+from spyder.plugins.completion.providers.languageserver.transport.stdio.producer import (
     StdioLanguageServerClient)
 from spyder.py3compat import getcwd
 
diff --git a/spyder/plugins/completion/snippets/__init__.py b/spyder/plugins/completion/providers/languageserver/transport/stdio/__init__.py
similarity index 100%
rename from spyder/plugins/completion/snippets/__init__.py
rename to spyder/plugins/completion/providers/languageserver/transport/stdio/__init__.py
diff --git a/spyder/plugins/completion/languageserver/transport/stdio/consumer.py b/spyder/plugins/completion/providers/languageserver/transport/stdio/consumer.py
similarity index 89%
rename from spyder/plugins/completion/languageserver/transport/stdio/consumer.py
rename to spyder/plugins/completion/providers/languageserver/transport/stdio/consumer.py
index 9fed64daa66..59ccfffe575 100644
--- a/spyder/plugins/completion/languageserver/transport/stdio/consumer.py
+++ b/spyder/plugins/completion/providers/languageserver/transport/stdio/consumer.py
@@ -15,7 +15,7 @@
 """
 
 import logging
-from spyder.plugins.completion.languageserver.transport.common.consumer import (
+from spyder.plugins.completion.providers.languageserver.transport.common.consumer import (
     IncomingMessageThread)
 
 logger = logging.getLogger(__name__)
diff --git a/spyder/plugins/completion/languageserver/transport/stdio/producer.py b/spyder/plugins/completion/providers/languageserver/transport/stdio/producer.py
similarity index 94%
rename from spyder/plugins/completion/languageserver/transport/stdio/producer.py
rename to spyder/plugins/completion/providers/languageserver/transport/stdio/producer.py
index 0b241c44f26..99acd917966 100644
--- a/spyder/plugins/completion/languageserver/transport/stdio/producer.py
+++ b/spyder/plugins/completion/providers/languageserver/transport/stdio/producer.py
@@ -21,9 +21,9 @@
 import logging
 
 # Local imports
-from spyder.plugins.completion.languageserver.transport.stdio.consumer import (
+from spyder.plugins.completion.providers.languageserver.transport.stdio.consumer import (
     StdioIncomingMessageThread)
-from spyder.plugins.completion.languageserver.transport.common.producer import (
+from spyder.plugins.completion.providers.languageserver.transport.common.producer import (
     LanguageServerClient)
 
 from pexpect import popen_spawn
diff --git a/spyder/plugins/completion/snippets/tests/__init__.py b/spyder/plugins/completion/providers/languageserver/transport/tcp/__init__.py
similarity index 100%
rename from spyder/plugins/completion/snippets/tests/__init__.py
rename to spyder/plugins/completion/providers/languageserver/transport/tcp/__init__.py
diff --git a/spyder/plugins/completion/languageserver/transport/tcp/consumer.py b/spyder/plugins/completion/providers/languageserver/transport/tcp/consumer.py
similarity index 89%
rename from spyder/plugins/completion/languageserver/transport/tcp/consumer.py
rename to spyder/plugins/completion/providers/languageserver/transport/tcp/consumer.py
index c465cf149c4..d3b23678aee 100644
--- a/spyder/plugins/completion/languageserver/transport/tcp/consumer.py
+++ b/spyder/plugins/completion/providers/languageserver/transport/tcp/consumer.py
@@ -14,7 +14,7 @@
 """
 
 import logging
-from spyder.plugins.completion.languageserver.transport.common.consumer import (
+from spyder.plugins.completion.providers.languageserver.transport.common.consumer import (
     IncomingMessageThread)
 
 logger = logging.getLogger(__name__)
diff --git a/spyder/plugins/completion/languageserver/transport/tcp/producer.py b/spyder/plugins/completion/providers/languageserver/transport/tcp/producer.py
similarity index 94%
rename from spyder/plugins/completion/languageserver/transport/tcp/producer.py
rename to spyder/plugins/completion/providers/languageserver/transport/tcp/producer.py
index c8c68cea02a..27ec8bdc040 100644
--- a/spyder/plugins/completion/languageserver/transport/tcp/producer.py
+++ b/spyder/plugins/completion/providers/languageserver/transport/tcp/producer.py
@@ -21,9 +21,9 @@
 import time
 
 # Local imports
-from spyder.plugins.completion.languageserver.transport.tcp.consumer import (
+from spyder.plugins.completion.providers.languageserver.transport.tcp.consumer import (
     TCPIncomingMessageThread)
-from spyder.plugins.completion.languageserver.transport.common.producer import (
+from spyder.plugins.completion.providers.languageserver.transport.common.producer import (
     LanguageServerClient)
 from spyder.py3compat import ConnectionError, BrokenPipeError
 
diff --git a/spyder/plugins/completion/languageserver/widgets/__init__.py b/spyder/plugins/completion/providers/languageserver/widgets/__init__.py
similarity index 100%
rename from spyder/plugins/completion/languageserver/widgets/__init__.py
rename to spyder/plugins/completion/providers/languageserver/widgets/__init__.py
diff --git a/spyder/plugins/completion/providers/languageserver/widgets/message_box.py b/spyder/plugins/completion/providers/languageserver/widgets/message_box.py
new file mode 100644
index 00000000000..4ab15cbca8c
--- /dev/null
+++ b/spyder/plugins/completion/providers/languageserver/widgets/message_box.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Language Server Protocol message boxes."""
+
+# Standard library imports
+import os
+
+# Third party imports
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.config.base import _
+from spyder.widgets.helperwidgets import MessageCheckBox
+
+
+class ServerDisabledMessageBox(MessageCheckBox):
+    sig_restart_spyder = Signal()
+
+    def __init__(self, parent, warn_str, set_option):
+        super().__init__(icon=QMessageBox.Warning, parent=parent)
+        self.set_option = set_option
+
+        self.setWindowTitle(_("Warning"))
+        self.set_checkbox_text(_("Don't show again"))
+        self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
+        self.setDefaultButton(QMessageBox.No)
+        self.set_checked(False)
+        self.set_check_visible(True)
+        self.setText(warn_str)
+
+    def exec_(self):
+        answer = super().exec_()
+        self.set_option('show_lsp_down_warning', not self.is_checked())
+        if answer == QMessageBox.Yes:
+            self.sig_restart_spyder.emit()
+
+    @classmethod
+    def instance(cls, warn_str, set_option):
+        def wrapper(parent):
+            return cls(parent, warn_str, set_option)
+        return wrapper
diff --git a/spyder/plugins/completion/languageserver/widgets/serversconfig.py b/spyder/plugins/completion/providers/languageserver/widgets/serversconfig.py
similarity index 90%
rename from spyder/plugins/completion/languageserver/widgets/serversconfig.py
rename to spyder/plugins/completion/providers/languageserver/widgets/serversconfig.py
index cf9a5bf2170..affdca73b58 100644
--- a/spyder/plugins/completion/languageserver/widgets/serversconfig.py
+++ b/spyder/plugins/completion/providers/languageserver/widgets/serversconfig.py
@@ -23,22 +23,25 @@
 
 # Local imports
 from spyder.config.base import _
-from spyder.config.manager import CONF
 from spyder.config.gui import get_font
-from spyder.plugins.completion.manager.api import LSP_LANGUAGES
+from spyder.plugins.completion.api import SUPPORTED_LANGUAGES
 from spyder.utils.misc import check_connection_port
 from spyder.utils.programs import find_program
 from spyder.widgets.helperwidgets import ItemDelegate
 from spyder.widgets.simplecodeeditor import SimpleCodeEditor
 
-LSP_LANGUAGE_NAME = {x.lower(): x for x in LSP_LANGUAGES}
-LANGUAGE_SET = {lang.lower() for lang in LSP_LANGUAGES}
+LSP_LANGUAGE_NAME = {x.lower(): x for x in SUPPORTED_LANGUAGES}
+LANGUAGE_SET = {lang.lower() for lang in SUPPORTED_LANGUAGES}
 
 
-def iter_servers():
-    for option in CONF.options('lsp-server'):
-        if option in LANGUAGE_SET:
-            server = LSPServer(language=option)
+def iter_servers(get_option, set_option, remove_option):
+    for language in LANGUAGE_SET:
+        conf = get_option(language, default=None)
+        if conf is not None:
+            server = LSPServer(language=language,
+                               set_option=set_option,
+                               get_option=get_option,
+                               remove_option=remove_option)
             server.load()
             yield server
 
@@ -48,7 +51,8 @@ class LSPServer(object):
 
     def __init__(self, language=None, cmd='', host='127.0.0.1',
                  port=2084, args='', external=False, stdio=False,
-                 configurations={}):
+                 configurations={}, set_option=None, get_option=None,
+                 remove_option=None):
         self.index = 0
         self.language = language
         if self.language in LSP_LANGUAGE_NAME:
@@ -60,6 +64,9 @@ def __init__(self, language=None, cmd='', host='127.0.0.1',
         self.host = host
         self.external = external
         self.stdio = stdio
+        self.set_option = set_option
+        self.get_option = get_option
+        self.remove_option = remove_option
 
     def __repr__(self):
         base_str = '[{0}] {1} {2} ({3}:{4})'
@@ -81,18 +88,22 @@ def __unicode__(self):
 
     def load(self):
         if self.language is not None:
-            state = CONF.get('lsp-server', self.language.lower())
+            state = self.get_option(self.language.lower())
             self.__dict__.update(state)
 
     def save(self):
         if self.language is not None:
             language = self.language.lower()
-            CONF.set('lsp-server', language, self.__dict__)
+            dict_repr = dict(self.__dict__)
+            dict_repr.pop('set_option')
+            dict_repr.pop('get_option')
+            dict_repr.pop('remove_option')
+            self.set_option(language, dict_repr)
 
     def delete(self):
         if self.language is not None:
             language = self.language.lower()
-            CONF.remove_option('lsp-server', language)
+            self.remove_option(language)
 
 
 class LSPServerEditor(QDialog):
@@ -113,7 +124,8 @@ class LSPServerEditor(QDialog):
 
     def __init__(self, parent, language=None, cmd='', host='127.0.0.1',
                  port=2084, args='', external=False, stdio=False,
-                 configurations={}, **kwargs):
+                 configurations={}, get_option=None, set_option=None,
+                 remove_option=None, **kwargs):
         super(LSPServerEditor, self).__init__(parent)
 
         description = _(
@@ -130,6 +142,9 @@ def __init__(self, parent, language=None, cmd='', host='127.0.0.1',
         )
         self.parent = parent
         self.external = external
+        self.set_option = set_option
+        self.get_option = get_option
+        self.remove_option = remove_option
 
         # Widgets
         self.server_settings_description = QLabel(description)
@@ -161,11 +176,11 @@ def __init__(self, parent, language=None, cmd='', host='127.0.0.1',
         self.lang_cb.setToolTip(
             _('Programming language provided by the LSP server'))
         self.lang_cb.addItem(_('Select a language'))
-        self.lang_cb.addItems(LSP_LANGUAGES)
+        self.lang_cb.addItems(SUPPORTED_LANGUAGES)
 
         self.button_ok.setEnabled(False)
         if language is not None:
-            idx = LSP_LANGUAGES.index(language)
+            idx = SUPPORTED_LANGUAGES.index(language)
             self.lang_cb.setCurrentIndex(idx + 1)
             self.button_ok.setEnabled(True)
 
@@ -189,7 +204,7 @@ def __init__(self, parent, language=None, cmd='', host='127.0.0.1',
 
         self.conf_input.setup_editor(
             language='json',
-            color_scheme=CONF.get('appearance', 'selected'),
+            color_scheme=get_option('selected', section='appearance'),
             wrap=False,
             highlight_current_line=True,
             font=get_font()
@@ -368,7 +383,7 @@ def lang_selection_changed(self):
             self.form_status(False)
             self.button_ok.setEnabled(False)
         else:
-            server = self.parent.get_server_by_lang(LSP_LANGUAGES[idx - 1])
+            server = self.parent.get_server_by_lang(SUPPORTED_LANGUAGES[idx - 1])
             self.form_status(True)
             if server is not None:
                 self.host_input.setText(server.host)
@@ -447,7 +462,7 @@ def set_stdio_options(self, enabled):
 
     def get_options(self):
         language_idx = self.lang_cb.currentIndex()
-        language = LSP_LANGUAGES[language_idx - 1]
+        language = SUPPORTED_LANGUAGES[language_idx - 1]
         host = self.host_input.text()
         port = int(self.port_spinner.value())
         external = self.external_cb.isChecked()
@@ -457,7 +472,10 @@ def get_options(self):
         configurations = json.loads(self.conf_input.toPlainText())
         server = LSPServer(language=language.lower(), cmd=cmd, args=args,
                            host=host, port=port, external=external,
-                           stdio=stdio, configurations=configurations)
+                           stdio=stdio, configurations=configurations,
+                           get_option=self.get_option,
+                           set_option=self.set_option,
+                           remove_option=self.remove_option)
         return server
 
 
@@ -609,7 +627,9 @@ def get_server_by_lang(self, lang):
         return self.source_model.server_map.get(lang)
 
     def load_servers(self):
-        servers = list(iter_servers())
+        servers = list(iter_servers(self._parent.get_option,
+                                    self._parent.set_option,
+                                    self._parent.remove_option))
         for i, server in enumerate(servers):
             server.index = i
             server.language = LSP_LANGUAGE_NAME[server.language.lower()]
@@ -621,11 +641,15 @@ def load_servers(self):
         self.sortByColumn(LANGUAGE, Qt.AscendingOrder)
 
     def save_servers(self):
+        language_set = set({})
         for server in self.source_model.servers:
+            language_set |= {server.language.lower()}
             server.save()
         while len(self.delete_queue) > 0:
             server = self.delete_queue.pop(0)
+            language_set |= {server.language.lower()}
             server.delete()
+        return language_set
 
     def delete_server(self, idx):
         server = self.source_model.servers.pop(idx)
@@ -635,8 +659,16 @@ def delete_server(self, idx):
         self.adjust_cells()
         self.sortByColumn(LANGUAGE, Qt.AscendingOrder)
 
+    def delete_server_by_lang(self, language):
+        idx = next((i for i, x in enumerate(self.source_model.servers)
+                    if x.language == language), None)
+        if idx is not None:
+            self.delete_server(idx)
+
     def show_editor(self, new_server=False):
-        server = LSPServer()
+        server = LSPServer(get_option=self._parent.get_option,
+                           set_option=self._parent.set_option,
+                           remove_option=self._parent.remove_option)
         if not new_server:
             idx = self.currentIndex().row()
             server = self.source_model.row(idx)
diff --git a/spyder/plugins/completion/languageserver/widgets/status.py b/spyder/plugins/completion/providers/languageserver/widgets/status.py
similarity index 76%
rename from spyder/plugins/completion/languageserver/widgets/status.py
rename to spyder/plugins/completion/providers/languageserver/widgets/status.py
index e36eaceeb2e..9b9af30a7fe 100644
--- a/spyder/plugins/completion/languageserver/widgets/status.py
+++ b/spyder/plugins/completion/providers/languageserver/widgets/status.py
@@ -50,11 +50,12 @@ class LSPStatusWidget(StatusBarWidget):
 
     STATUS = "LSP {}: {}"
 
-    def __init__(self, parent, plugin):
+    def __init__(self, parent, provider):
         self.tooltip = self.BASE_TOOLTIP
         super().__init__(parent, spinner=True)
 
-        self.plugin = plugin
+        self.provider = provider
+        self.current_language = None
         self.menu = QMenu(self)
 
         # Setup
@@ -66,7 +67,7 @@ def __init__(self, parent, plugin):
     def show_menu(self):
         """Display a menu when clicking on the widget."""
         menu = self.menu
-        language = self.get_current_editor_language().lower()
+        language = self.current_language.lower()
 
         if language is not None:
             menu.clear()
@@ -75,7 +76,7 @@ def show_menu(self):
             restart_action = create_action(
                 self,
                 text=text,
-                triggered=lambda: self.plugin.restart_lsp(language, force=True),
+                triggered=lambda: self.provider.restart_lsp(language, force=True),
             )
             add_actions(menu, [restart_action])
             rect = self.contentsRect()
@@ -100,7 +101,7 @@ def set_status(self, lsp_language=None, status=None):
             status = ClientStatus.STRINGS_FOR_TRANSLATION[status]
 
         if lsp_language is not None:
-            status = self.STATUS.format(lsp_language, status)
+            status = self.STATUS.format(lsp_language.capitalize(), status)
         self.set_value(status)
 
     def get_tooltip(self):
@@ -113,37 +114,28 @@ def get_icon(self):
     @Slot()
     def update_status(self, lsp_language=None, status=None):
         """Update status message."""
-        editor_language = self.get_current_editor_language()
-
         # This case can only happen when switching files in the editor
         if lsp_language is None and status is None:
-            lsp_language = editor_language.lower()
-            if not self.plugin.clients.get(lsp_language, None):
+            lsp_language = self.current_language.lower()
+            if not self.provider.clients.get(lsp_language, None):
                 self.setVisible(False)
             else:
-                status = self.plugin.clients_statusbar.get(
+                status = self.provider.clients_statusbar.get(
                     lsp_language,
                     ClientStatus.STARTING
                 )
-                self.set_status(editor_language, status)
+                self.set_status(lsp_language, status)
                 self.setVisible(True)
             return
 
         # Don't update the status in case the editor and LSP languages
         # are different.
-        if editor_language.lower() != lsp_language:
+        if self.current_language.lower() != lsp_language:
             return
         else:
-            self.set_status(editor_language, status)
+            self.set_status(self.current_language, status)
             self.setVisible(True)
 
-    def get_current_editor_language(self):
-        """Get current LSP language."""
-        main = self.plugin.main
-        language = _('Unknown')
-
-        if main and main.editor:
-            codeeditor = main.editor.get_current_editor()
-            if codeeditor is not None:
-                language = codeeditor.language
-        return language
+    def set_current_language(self, language):
+        self.current_language = language
+        self.update_status()
diff --git a/spyder/plugins/completion/providers/snippets/__init__.py b/spyder/plugins/completion/providers/snippets/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/spyder/plugins/completion/snippets/actor.py b/spyder/plugins/completion/providers/snippets/actor.py
similarity index 93%
rename from spyder/plugins/completion/snippets/actor.py
rename to spyder/plugins/completion/providers/snippets/actor.py
index d9954f4f0e1..e3a036f5ccf 100644
--- a/spyder/plugins/completion/snippets/actor.py
+++ b/spyder/plugins/completion/providers/snippets/actor.py
@@ -19,9 +19,9 @@
 from qtpy.QtCore import QObject, QThread, QMutex, QMutexLocker, Signal, Slot
 
 # Local imports
-from spyder.plugins.completion.manager.api import CompletionItemKind
-from spyder.plugins.completion.manager.api import LSPRequestTypes
-from spyder.plugins.completion.snippets.trie import Trie
+from spyder.plugins.completion.api import CompletionItemKind
+from spyder.plugins.completion.api import CompletionRequestTypes
+from spyder.plugins.completion.providers.snippets.trie import Trie
 
 
 SNIPPETS_COMPLETION = "Snippets"
@@ -82,7 +82,7 @@ def handle_msg(self, message):
         msg_type, _id, file, msg = [
             message[k] for k in ('type', 'id', 'file', 'msg')]
         logger.debug(u'Perform request {0} with id {1}'.format(msg_type, _id))
-        if msg_type == LSPRequestTypes.DOCUMENT_COMPLETION:
+        if msg_type == CompletionRequestTypes.DOCUMENT_COMPLETION:
             language = msg['language']
             current_word = msg['current_word']
             snippets = []
diff --git a/spyder/plugins/completion/providers/snippets/conftabs.py b/spyder/plugins/completion/providers/snippets/conftabs.py
new file mode 100644
index 00000000000..e0f80a28d46
--- /dev/null
+++ b/spyder/plugins/completion/providers/snippets/conftabs.py
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Text snippets configuration tabs.
+"""
+
+# Standard library imports
+import copy
+import os.path as osp
+
+# Third party imports
+from qtpy.compat import getsavefilename, getopenfilename
+from qtpy.QtCore import Qt, Slot
+from qtpy.QtWidgets import (QComboBox, QGroupBox, QGridLayout, QLabel,
+                            QMessageBox, QPushButton, QTabWidget, QVBoxLayout,
+                            QWidget, QFileDialog)
+
+# Local imports
+from spyder.config.base import _
+from spyder.config.snippets import SNIPPETS
+from spyder.plugins.completion.providers.snippets.widgets.snippetsconfig import (
+    SnippetModelsProxy, SnippetTable, SUPPORTED_LANGUAGES_PY, PYTHON_POS)
+from spyder.api.preferences import SpyderPreferencesTab
+
+LSP_URL = "https://microsoft.github.io/language-server-protocol"
+
+
+class SnippetsConfigTab(SpyderPreferencesTab):
+    TITLE = _('Snippets')
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        self.snippets_language = 'python'
+        grammar_url = (
+            "<a href=\"{0}/specifications/specification-current#snippet_syntax\">"
+            "{1}</a>".format(LSP_URL, _('the LSP grammar')))
+        snippets_info_label = QLabel(
+            _("Spyder allows to define custom completion snippets to use "
+              "in addition to the ones offered by the Language Server "
+              "Protocol (LSP). Each snippet should follow {}.<br><br> "
+              "<b>Note:</b> All changes will be effective only when applying "
+              "the settings").format(grammar_url))
+        snippets_info_label.setOpenExternalLinks(True)
+        snippets_info_label.setWordWrap(True)
+        snippets_info_label.setAlignment(Qt.AlignJustify)
+
+        self.snippets_language_cb = QComboBox(self)
+        self.snippets_language_cb.setToolTip(
+            _('Programming language provided by the LSP server'))
+        self.snippets_language_cb.addItems(SUPPORTED_LANGUAGES_PY)
+        self.snippets_language_cb.setCurrentIndex(PYTHON_POS)
+        self.snippets_language_cb.currentTextChanged.connect(
+            self.change_language_snippets)
+
+        snippet_lang_group = QGroupBox(_('Language'))
+        snippet_lang_layout = QVBoxLayout()
+        snippet_lang_layout.addWidget(self.snippets_language_cb)
+        snippet_lang_group.setLayout(snippet_lang_layout)
+
+        self.snippets_proxy = SnippetModelsProxy(self)
+        self.snippets_table = SnippetTable(
+            self, self.snippets_proxy, language=self.snippets_language)
+        self.snippets_table.setMaximumHeight(180)
+
+        snippet_table_group = QGroupBox(_('Available snippets'))
+        snippet_table_layout = QVBoxLayout()
+        snippet_table_layout.addWidget(self.snippets_table)
+        snippet_table_group.setLayout(snippet_table_layout)
+
+        # Buttons
+        self.reset_snippets_btn = QPushButton(_("Reset to default values"))
+        self.new_snippet_btn = QPushButton(_("Create a new snippet"))
+        self.delete_snippet_btn = QPushButton(
+            _("Delete currently selected snippet"))
+        self.delete_snippet_btn.setEnabled(False)
+        self.export_snippets_btn = QPushButton(_("Export snippets to JSON"))
+        self.import_snippets_btn = QPushButton(_("Import snippets from JSON"))
+
+        # Slots connected to buttons
+        self.new_snippet_btn.clicked.connect(self.create_new_snippet)
+        self.reset_snippets_btn.clicked.connect(self.reset_default_snippets)
+        self.delete_snippet_btn.clicked.connect(self.delete_snippet)
+        self.export_snippets_btn.clicked.connect(self.export_snippets)
+        self.import_snippets_btn.clicked.connect(self.import_snippets)
+
+        # Buttons layout
+        btns = [self.new_snippet_btn,
+                self.delete_snippet_btn,
+                self.reset_snippets_btn,
+                self.export_snippets_btn,
+                self.import_snippets_btn]
+        sn_buttons_layout = QGridLayout()
+        for i, btn in enumerate(btns):
+            sn_buttons_layout.addWidget(btn, i, 1)
+        sn_buttons_layout.setColumnStretch(0, 1)
+        sn_buttons_layout.setColumnStretch(1, 2)
+        sn_buttons_layout.setColumnStretch(2, 1)
+
+        # Snippets layout
+        snippets_layout = QVBoxLayout()
+        snippets_layout.addWidget(snippets_info_label)
+        snippets_layout.addWidget(snippet_lang_group)
+        snippets_layout.addWidget(snippet_table_group)
+        snippets_layout.addLayout(sn_buttons_layout)
+
+        self.setLayout(snippets_layout)
+
+    def create_new_snippet(self):
+        self.snippets_table.show_editor(new_snippet=True)
+
+    def delete_snippet(self):
+        idx = self.snippets_table.currentIndex().row()
+        self.snippets_table.delete_snippet(idx)
+        self.set_modified(True)
+        self.delete_snippet_btn.setEnabled(False)
+
+    def reset_default_snippets(self):
+        language = self.snippets_language_cb.currentText()
+        default_snippets_lang = copy.deepcopy(
+            SNIPPETS.get(language.lower(), {}))
+        self.snippets_proxy.reload_model(
+            language.lower(), default_snippets_lang)
+        self.snippets_table.reset_plain()
+        self.set_modified(True)
+
+    def change_language_snippets(self, language):
+        self.snippets_table.update_language_model(language)
+
+    def export_snippets(self):
+        filename, _selfilter = getsavefilename(
+            self, _("Save snippets"),
+            'spyder_snippets.json',
+            filters='JSON (*.json)',
+            selectedfilter='',
+            options=QFileDialog.HideNameFilterDetails)
+
+        if filename:
+            filename = osp.normpath(filename)
+            self.snippets_proxy.export_snippets(filename)
+
+    def import_snippets(self):
+        filename, _sf = getopenfilename(
+            self,
+            _("Load snippets"),
+            filters='JSON (*.json)',
+            selectedfilter='',
+            options=QFileDialog.HideNameFilterDetails,
+        )
+
+        if filename:
+            filename = osp.normpath(filename)
+            valid, total, errors = self.snippets_proxy.import_snippets(
+                filename)
+            modified = True
+            if len(errors) == 0:
+                QMessageBox.information(
+                    self,
+                    _('All snippets imported'),
+                    _('{0} snippets were loaded successfully').format(valid),
+                    QMessageBox.Ok)
+            else:
+                if 'loading' in errors:
+                    modified = False
+                    QMessageBox.critical(
+                        self,
+                        _('JSON malformed'),
+                        _('There was an error when trying to load the '
+                          'provided JSON file: <tt>{0}</tt>').format(
+                              errors['loading']),
+                        QMessageBox.Ok
+                    )
+                elif 'validation' in errors:
+                    modified = False
+                    QMessageBox.critical(
+                        self,
+                        _('Invalid snippet file'),
+                        _('The provided snippet file does not comply with '
+                          'the Spyder JSON snippets spec and therefore it '
+                          'cannot be loaded.<br><br><tt>{}</tt>').format(
+                              errors['validation']),
+                        QMessageBox.Ok
+                    )
+                elif 'syntax' in errors:
+                    syntax_errors = errors['syntax']
+                    msg = []
+                    for syntax_key in syntax_errors:
+                        syntax_err = syntax_errors[syntax_key]
+                        msg.append('<b>{0}</b>: {1}'.format(
+                            syntax_key, syntax_err))
+                    err_msg = '<br>'.join(msg)
+
+                    QMessageBox.warning(
+                        self,
+                        _('Incorrect snippet format'),
+                        _('Spyder was able to load {0}/{1} snippets '
+                          'correctly, please check the following snippets '
+                          'for any syntax errors: '
+                          '<br><br>{2}').format(valid, total, err_msg),
+                        QMessageBox.Ok
+                    )
+            self.set_modified(modified)
+
+    def apply_settings(self):
+        return self.snippets_proxy.save_snippets()
diff --git a/spyder/plugins/completion/providers/snippets/provider.py b/spyder/plugins/completion/providers/snippets/provider.py
new file mode 100644
index 00000000000..578e352503b
--- /dev/null
+++ b/spyder/plugins/completion/providers/snippets/provider.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Custom text snippets completion plugin."""
+
+# Standard library imports
+import bisect
+import logging
+
+# Local imports
+from spyder.config.base import _
+from spyder.config.snippets import SNIPPETS
+from spyder.plugins.completion.api import (SpyderCompletionProvider,
+                                           SUPPORTED_LANGUAGES)
+from spyder.plugins.completion.providers.snippets.actor import SnippetsActor
+from spyder.plugins.completion.providers.snippets.conftabs import (
+    SnippetsConfigTab
+)
+
+PYTHON_POS = bisect.bisect_left(SUPPORTED_LANGUAGES, 'Python')
+SUPPORTED_LANGUAGES_PY = list(SUPPORTED_LANGUAGES)
+SUPPORTED_LANGUAGES_PY.insert(PYTHON_POS, 'Python')
+SUPPORTED_LANGUAGES_PY = {x.lower() for x in SUPPORTED_LANGUAGES_PY}
+
+logger = logging.getLogger(__name__)
+
+
+class SnippetsProvider(SpyderCompletionProvider):
+    COMPLETION_CLIENT_NAME = 'snippets'
+    DEFAULT_ORDER = 2
+    CONF_DEFAULTS = [(lang, SNIPPETS[lang]) for lang in SNIPPETS]
+    CONF_VERSION = "0.1.0"
+    CONF_TABS = [SnippetsConfigTab]
+
+    def __init__(self, parent, config):
+        SpyderCompletionProvider.__init__(self, parent, config)
+        self.snippets_actor = SnippetsActor(self)
+        self.snippets_actor.sig_snippets_ready.connect(
+            self.signal_provider_ready)
+        self.snippets_actor.sig_snippets_response.connect(
+            lambda _id, resp: self.sig_response_ready.emit(
+                self.COMPLETION_CLIENT_NAME, _id, resp))
+        self.started = False
+        self.requests = {}
+        self.config = config
+
+    def get_name(self):
+        return _('Custom text snippets')
+
+    def start_provider(self, language):
+        return self.started
+
+    def start(self):
+        if not self.started:
+            self.snippets_actor.start()
+            self.started = True
+
+    def signal_provider_ready(self):
+        self.update_configuration(self.config)
+        self.sig_provider_ready.emit(self.COMPLETION_CLIENT_NAME)
+
+    def shutdown(self):
+        if self.started:
+            self.snippets_actor.stop()
+            self.started = False
+
+    def send_request(self, language, req_type, req, req_id=None):
+        request = {
+            'type': req_type,
+            'file': req['file'],
+            'id': req_id,
+            'msg': req
+        }
+        req['language'] = language
+        self.snippets_actor.sig_mailbox.emit(request)
+
+    def update_configuration(self, snippets):
+        self.config = snippets
+        snippet_info = {}
+        for language in SUPPORTED_LANGUAGES_PY:
+            snippet_info[language] = snippets.get(language, {})
+        self.snippets_actor.sig_update_snippets.emit(snippet_info)
diff --git a/spyder/plugins/completion/providers/snippets/tests/__init__.py b/spyder/plugins/completion/providers/snippets/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/spyder/plugins/completion/snippets/tests/conftest.py b/spyder/plugins/completion/providers/snippets/tests/conftest.py
similarity index 100%
rename from spyder/plugins/completion/snippets/tests/conftest.py
rename to spyder/plugins/completion/providers/snippets/tests/conftest.py
diff --git a/spyder/plugins/completion/snippets/tests/test_snippets.py b/spyder/plugins/completion/providers/snippets/tests/test_snippets.py
similarity index 100%
rename from spyder/plugins/completion/snippets/tests/test_snippets.py
rename to spyder/plugins/completion/providers/snippets/tests/test_snippets.py
diff --git a/spyder/plugins/completion/snippets/trie.py b/spyder/plugins/completion/providers/snippets/trie.py
similarity index 100%
rename from spyder/plugins/completion/snippets/trie.py
rename to spyder/plugins/completion/providers/snippets/trie.py
diff --git a/spyder/plugins/completion/providers/snippets/widgets/__init__.py b/spyder/plugins/completion/providers/snippets/widgets/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/spyder/plugins/completion/languageserver/widgets/snippetsconfig.py b/spyder/plugins/completion/providers/snippets/widgets/snippetsconfig.py
similarity index 91%
rename from spyder/plugins/completion/languageserver/widgets/snippetsconfig.py
rename to spyder/plugins/completion/providers/snippets/widgets/snippetsconfig.py
index c4e0983aa75..0c098864201 100644
--- a/spyder/plugins/completion/languageserver/widgets/snippetsconfig.py
+++ b/spyder/plugins/completion/providers/snippets/widgets/snippetsconfig.py
@@ -25,19 +25,19 @@
 from spyder.config.base import _
 from spyder.config.manager import CONF
 from spyder.config.gui import get_font
-from spyder.plugins.completion.manager.api import LSP_LANGUAGES
+from spyder.plugins.completion.api import SUPPORTED_LANGUAGES
 from spyder.utils.snippets.ast import build_snippet_ast
 from spyder.widgets.helperwidgets import ItemDelegate
 from spyder.widgets.simplecodeeditor import SimpleCodeEditor
 
 
 # Languages supported by the text snippets extension
-LSP_LANGUAGE_NAME = {x.lower(): x for x in LSP_LANGUAGES}
-LANGUAGE_SET = {lang.lower() for lang in LSP_LANGUAGES}
+LANGUAGE_NAMES = {x.lower(): x for x in SUPPORTED_LANGUAGES}
+LANGUAGE_SET = {lang.lower() for lang in SUPPORTED_LANGUAGES}
 
-PYTHON_POS = bisect.bisect_left(LSP_LANGUAGES, 'Python')
-LSP_LANGUAGES_PY = list(LSP_LANGUAGES)
-LSP_LANGUAGES_PY.insert(PYTHON_POS, 'Python')
+PYTHON_POS = bisect.bisect_left(SUPPORTED_LANGUAGES, 'Python')
+SUPPORTED_LANGUAGES_PY = list(SUPPORTED_LANGUAGES)
+SUPPORTED_LANGUAGES_PY.insert(PYTHON_POS, 'Python')
 
 LANGUAGE, ADDR, CMD = [0, 1, 2]
 
@@ -51,7 +51,7 @@
             'language': {
                 'type': 'string',
                 'description': 'Programming language',
-                'enum': [lang.lower() for lang in LSP_LANGUAGES_PY]
+                'enum': [lang.lower() for lang in SUPPORTED_LANGUAGES_PY]
             },
             'triggers': {
                 'type': 'array',
@@ -109,17 +109,19 @@
 }
 
 
-def iter_snippets(language, snippets=None):
+def iter_snippets(language, get_option, set_option, snippets=None):
     language_snippets = []
     load_snippets = snippets is None
     if load_snippets:
-        snippets = CONF.get('snippet-completions', language.lower(), {})
+        snippets = get_option(language.lower(), default={})
     for trigger in snippets:
         trigger_descriptions = snippets[trigger]
         for description in trigger_descriptions:
             if load_snippets:
                 this_snippet = Snippet(language=language, trigger_text=trigger,
-                                       description=description)
+                                       description=description,
+                                       get_option=get_option,
+                                       set_option=set_option)
                 this_snippet.load()
             else:
                 current_snippet = trigger_descriptions[description]
@@ -129,7 +131,9 @@ def iter_snippets(language, snippets=None):
                                        trigger_text=trigger,
                                        description=description,
                                        snippet_text=text,
-                                       remove_trigger=remove_trigger)
+                                       remove_trigger=remove_trigger,
+                                       get_option=get_option,
+                                       set_option=set_option)
             language_snippets.append(this_snippet)
     return language_snippets
 
@@ -138,11 +142,12 @@ class Snippet:
     """Convenience class to store user snippets."""
 
     def __init__(self, language=None, trigger_text="", description="",
-                 snippet_text="", remove_trigger=False):
+                 snippet_text="", remove_trigger=False, get_option=None,
+                 set_option=None):
         self.index = 0
         self.language = language
-        if self.language in LSP_LANGUAGE_NAME:
-            self.language = LSP_LANGUAGE_NAME[self.language]
+        if self.language in LANGUAGE_NAMES:
+            self.language = LANGUAGE_NAMES[self.language]
 
         self.trigger_text = trigger_text
         self.snippet_text = snippet_text
@@ -150,6 +155,8 @@ def __init__(self, language=None, trigger_text="", description="",
         self.remove_trigger = remove_trigger
         self.initial_trigger_text = trigger_text
         self.initial_description = description
+        self.set_option = set_option
+        self.get_option = get_option
 
     def __repr__(self):
         return '[{0}] {1} ({2}): {3}'.format(
@@ -168,7 +175,7 @@ def update(self, trigger_text, description_text, snippet_text,
 
     def load(self):
         if self.language is not None and self.trigger_text != '':
-            state = CONF.get('snippet-completions', self.language.lower())
+            state = self.get_option(self.language.lower())
             trigger_info = state[self.trigger_text]
             snippet_info = trigger_info[self.description]
             self.snippet_text = snippet_info['text']
@@ -177,7 +184,7 @@ def load(self):
     def save(self):
         if self.language is not None:
             language = self.language.lower()
-            current_state = CONF.get('snippet-completions', language, {})
+            current_state = self.get_option(language, default={})
             new_state = {
                 'text': self.snippet_text,
                 'remove_trigger': self.remove_trigger
@@ -193,17 +200,17 @@ def save(self):
             trigger_info = current_state.get(self.trigger_text, {})
             trigger_info[self.description] = new_state
             current_state[self.trigger_text] = trigger_info
-            CONF.set('snippet-completions', language, current_state)
+            self.set_option(language, current_state)
 
     def delete(self):
         if self.language is not None:
             language = self.language.lower()
-            current_state = CONF.get('snippet-completions', language, {})
+            current_state = self.get_option(language, default={})
             trigger = current_state[self.trigger_text]
             trigger.pop(self.description)
             if len(trigger) == 0:
                 current_state.pop(self.trigger_text)
-            CONF.set('snippet-completions', language, current_state)
+            self.set_option(language, current_state)
 
 
 class SnippetEditor(QDialog):
@@ -217,7 +224,7 @@ class SnippetEditor(QDialog):
 
     def __init__(self, parent, language=None, trigger_text='', description='',
                  snippet_text='', remove_trigger=False, trigger_texts=[],
-                 descriptions=[]):
+                 descriptions=[], get_option=None, set_option=None):
         super(SnippetEditor, self).__init__(parent)
 
         snippet_description = _(
@@ -236,7 +243,8 @@ def __init__(self, parent, language=None, trigger_text='', description='',
         self.base_snippet = Snippet(
             language=language, trigger_text=trigger_text,
             snippet_text=snippet_text, description=description,
-            remove_trigger=remove_trigger)
+            remove_trigger=remove_trigger,
+            get_option=get_option, set_option=set_option)
 
         # Widgets
         self.snippet_settings_description = QLabel(snippet_description)
@@ -292,7 +300,7 @@ def __init__(self, parent, language=None, trigger_text='', description='',
 
         self.snippet_input.setup_editor(
             language=language,
-            color_scheme=CONF.get('appearance', 'selected'),
+            color_scheme=get_option('selected', section='appearance'),
             wrap=False,
             highlight_current_line=True,
             font=get_font()
@@ -479,9 +487,10 @@ def reset(self):
 
 
 class SnippetModelsProxy:
-    def __init__(self):
+    def __init__(self, parent):
         self.models = {}
         self.awaiting_queue = {}
+        self.parent = parent
 
     def get_model(self, table, language, text_color=None):
         if language not in self.models:
@@ -499,7 +508,9 @@ def reload_model(self, language, defaults):
             self.load_snippets(language, model, defaults)
 
     def load_snippets(self, language, model, snippets=None, to_add=[]):
-        snippets = iter_snippets(language, snippets=snippets)
+        snippets = iter_snippets(language, self.parent.get_option,
+                                 self.parent.set_option,
+                                 snippets=snippets)
         for i, snippet in enumerate(snippets):
             snippet.index = i
 
@@ -521,7 +532,9 @@ def load_snippets(self, language, model, snippets=None, to_add=[]):
         model.snippet_map = snippet_map
 
     def save_snippets(self):
+        language_changes = set({})
         for language in self.models:
+            language_changes |= {language}
             language_model = self.models[language]
 
             while len(language_model.delete_queue) > 0:
@@ -532,15 +545,19 @@ def save_snippets(self):
                 snippet.save()
 
         for language in list(self.awaiting_queue.keys()):
+            language_changes |= {language}
             language_queue = self.awaiting_queue.pop(language)
             for snippet in language_queue:
                 snippet.save()
+        return language_changes
 
     def update_or_enqueue(self, language, trigger, description, snippet):
         new_snippet = Snippet(
             language=language, trigger_text=trigger, description=description,
             snippet_text=snippet['text'],
-            remove_trigger=snippet['remove_trigger'])
+            remove_trigger=snippet['remove_trigger'],
+            get_option=self.parent.get_option,
+            set_option=self.parent.set_option)
 
         if language in self.models:
             language_model = self.models[language]
@@ -708,7 +725,8 @@ def delete_snippet(self, idx):
         self.sortByColumn(self.source_model.TRIGGER, Qt.AscendingOrder)
 
     def show_editor(self, new_snippet=False):
-        snippet = Snippet()
+        snippet = Snippet(get_option=self._parent.get_option,
+                          set_option=self._parent.set_option)
         if not new_snippet:
             idx = self.currentIndex().row()
             snippet = self.source_model.row(idx)
@@ -727,7 +745,9 @@ def show_editor(self, new_snippet=False):
                                remove_trigger=snippet.remove_trigger,
                                snippet_text=snippet.snippet_text,
                                trigger_texts=trigger_texts,
-                               descriptions=descriptions)
+                               descriptions=descriptions,
+                               get_option=self._parent.get_option,
+                               set_option=self._parent.set_option)
         if dialog.exec_():
             snippet = dialog.get_options()
             key = (snippet.trigger_text, snippet.description)
diff --git a/spyder/plugins/completion/snippets/plugin.py b/spyder/plugins/completion/snippets/plugin.py
deleted file mode 100644
index 34faa2140c0..00000000000
--- a/spyder/plugins/completion/snippets/plugin.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""Custom text snippets completion plugin."""
-
-# Standard library imports
-import bisect
-import logging
-
-# Local imports
-from spyder.plugins.completion.manager.api import (SpyderCompletionPlugin,
-                                                   LSP_LANGUAGES)
-from spyder.plugins.completion.snippets.actor import SnippetsActor
-
-PYTHON_POS = bisect.bisect_left(LSP_LANGUAGES, 'Python')
-LSP_LANGUAGES_PY = list(LSP_LANGUAGES)
-LSP_LANGUAGES_PY.insert(PYTHON_POS, 'Python')
-LSP_LANGUAGES_PY = {x.lower() for x in LSP_LANGUAGES_PY}
-
-logger = logging.getLogger(__name__)
-
-
-class SnippetsPlugin(SpyderCompletionPlugin):
-    CONF_SECTION = 'snippet-completions'
-    CONF_FILE = False
-    COMPLETION_CLIENT_NAME = 'snippets'
-
-    def __init__(self, parent):
-        SpyderCompletionPlugin.__init__(self, parent)
-        self.snippets_actor = SnippetsActor(self)
-        self.snippets_actor.sig_snippets_ready.connect(
-            lambda: self.sig_plugin_ready.emit(self.COMPLETION_CLIENT_NAME))
-        self.snippets_actor.sig_snippets_response.connect(
-            lambda _id, resp: self.sig_response_ready.emit(
-                self.COMPLETION_CLIENT_NAME, _id, resp))
-        self.started = False
-        self.requests = {}
-        self.update_configuration()
-
-    def start_client(self, language):
-        return self.started
-
-    def start(self):
-        if not self.started and self.enabled:
-            self.snippets_actor.start()
-            self.started = True
-
-    def shutdown(self):
-        if self.started:
-            self.snippets_actor.stop()
-
-    def send_request(self, language, req_type, req, req_id=None):
-        if not self.enabled:
-            return
-
-        request = {
-            'type': req_type,
-            'file': req['file'],
-            'id': req_id,
-            'msg': req
-        }
-        req['language'] = language
-        self.snippets_actor.sig_mailbox.emit(request)
-
-    def update_configuration(self):
-        self.enabled = self.get_option('enable')
-        snippet_info = {}
-        for language in LSP_LANGUAGES_PY:
-            snippet_info[language] = self.get_option(language, {})
-        self.snippets_actor.sig_update_snippets.emit(snippet_info)
diff --git a/spyder/plugins/editor/panels/classfunctiondropdown.py b/spyder/plugins/editor/panels/classfunctiondropdown.py
index 7f2a4c037b3..ae3089c4dd5 100644
--- a/spyder/plugins/editor/panels/classfunctiondropdown.py
+++ b/spyder/plugins/editor/panels/classfunctiondropdown.py
@@ -15,7 +15,7 @@
 # Local imports
 from spyder.api.panel import Panel
 from spyder.config.base import _
-from spyder.plugins.completion.manager.api import SymbolKind
+from spyder.plugins.completion.api import SymbolKind
 from spyder.utils import icon_manager as ima
 
 
diff --git a/spyder/plugins/editor/panels/linenumber.py b/spyder/plugins/editor/panels/linenumber.py
index 4937ba81aed..002051809f2 100644
--- a/spyder/plugins/editor/panels/linenumber.py
+++ b/spyder/plugins/editor/panels/linenumber.py
@@ -21,7 +21,7 @@
 from spyder.utils import icon_manager as ima
 from spyder.utils.programs import check_version
 from spyder.api.panel import Panel
-from spyder.plugins.completion.manager.api import DiagnosticSeverity
+from spyder.plugins.completion.api import DiagnosticSeverity
 
 
 QT55_VERSION = check_version(QT_VERSION, "5.5", ">=")
diff --git a/spyder/plugins/editor/panels/scrollflag.py b/spyder/plugins/editor/panels/scrollflag.py
index 598664d0ae0..593fb14dcac 100644
--- a/spyder/plugins/editor/panels/scrollflag.py
+++ b/spyder/plugins/editor/panels/scrollflag.py
@@ -20,7 +20,7 @@
 # Local imports
 from spyder.api.panel import Panel
 from spyder.config.gui import is_dark_interface
-from spyder.plugins.completion.manager.api import DiagnosticSeverity
+from spyder.plugins.completion.api import DiagnosticSeverity
 
 
 REFRESH_RATE = 1000
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
index 3538bf81f18..8b10c10d9e2 100644
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -104,7 +104,7 @@ class Editor(SpyderPluginWidget):
     sig_file_debug_message_requested = Signal()
 
     # This signal is fired for any focus change among all editor stacks
-    sig_editor_focus_changed = Signal()
+    sig_editor_focus_changed = Signal(str, str)
 
     sig_help_requested = Signal(dict)
     """
@@ -317,7 +317,7 @@ def report_open_file(self, options):
         language = options['language']
         codeeditor = options['codeeditor']
 
-        status = self.main.completions.start_client(language.lower())
+        status = self.main.completions.start_provider(language.lower())
         self.main.completions.register_file(
             language.lower(), filename, codeeditor)
         if status:
@@ -327,10 +327,10 @@ def report_open_file(self, options):
                 codeeditor.register_completion_capabilities(
                     self.completion_capabilities[language.lower()])
                 codeeditor.start_completion_services()
-            elif self.main.completions.is_fallback_only(language.lower()):
-                # This is required to use fallback completions for files
-                # without a language server.
-                codeeditor.start_completion_services()
+            # elif self.main.completions.is_fallback_only(language.lower()):
+            # This is required to use fallback completions for files
+            # without a language server.
+            codeeditor.start_completion_services()
         else:
             if codeeditor.language == language.lower():
                 logger.debug('Setting {0} completions off'.format(filename))
@@ -1182,6 +1182,11 @@ def register_plugin(self):
         self.main.console.sig_edit_goto_requested.connect(self.load)
         self.exec_in_extconsole.connect(self.main.execute_in_external_console)
         self.redirect_stdio.connect(self.main.redirect_internalshell_stdio)
+        self.main.completions.sig_language_client_available.connect(
+            self.register_completion_capabilities)
+
+        self.sig_editor_focus_changed.connect(
+            self.main.completions.editor_focus_changed)
 
         if self.main.outlineexplorer is not None:
             self.set_outlineexplorer(self.main.outlineexplorer)
@@ -1422,8 +1427,10 @@ def register_editorstack(self, editorstack):
                                               run_cell_copy))
         editorstack.update_plugin_title.connect(
                                    lambda: self.sig_update_plugin_title.emit())
-        editorstack.editor_focus_changed.connect(self.save_focus_editorstack)
-        editorstack.editor_focus_changed.connect(self.main.plugin_focus_changed)
+        editorstack.editor_focus_changed.connect(
+            lambda _x, _y: self.save_focus_editorstack)
+        editorstack.editor_focus_changed.connect(
+            lambda _x, _y: self.main.plugin_focus_changed)
         editorstack.editor_focus_changed.connect(self.sig_editor_focus_changed)
         editorstack.zoom_in.connect(lambda: self.zoom(1))
         editorstack.zoom_out.connect(lambda: self.zoom(-1))
diff --git a/spyder/plugins/editor/utils/switcher.py b/spyder/plugins/editor/utils/switcher.py
index 01dc4370333..0226d0f2ece 100644
--- a/spyder/plugins/editor/utils/switcher.py
+++ b/spyder/plugins/editor/utils/switcher.py
@@ -19,8 +19,7 @@
 from spyder.config.manager import CONF
 from spyder.utils import icon_manager as ima
 from spyder.utils.switcher import shorten_paths, get_file_icon
-from spyder.plugins.completion.manager.api import (
-    SymbolKind, BLOCK_COMMENT, CELL, SYMBOL_KIND_ICON)
+from spyder.plugins.completion.api import SymbolKind, SYMBOL_KIND_ICON
 
 
 class EditorSwitcherManager(object):
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
index 5677486075c..e54e3a826ca 100644
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -63,11 +63,11 @@
                                               QMenuOnlyForEnter,
                                               EditorExtensionsManager,
                                               SnippetsExtension)
-from spyder.plugins.completion.kite.widgets.calltoaction import (
-    KiteCallToAction)
-from spyder.plugins.completion.manager.api import (LSPRequestTypes,
-                                                   TextDocumentSyncKind,
-                                                   DiagnosticSeverity)
+# from spyder.plugins.completion.kite.widgets.calltoaction import (
+#     KiteCallToAction)
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           TextDocumentSyncKind,
+                                           DiagnosticSeverity)
 from spyder.plugins.editor.panels import (ClassFunctionDropdown,
                                           DebuggerPanel, EdgeLine,
                                           FoldingPanel, IndentationGuide,
@@ -80,7 +80,7 @@
 from spyder.plugins.editor.utils.languages import ALL_LANGUAGES, CELL_LANGUAGES
 from spyder.plugins.editor.panels.utils import (
     merge_folding, collect_folding_regions)
-from spyder.plugins.completion.manager.decorators import (
+from spyder.plugins.completion.decorators import (
     request, handles, class_register)
 from spyder.plugins.editor.widgets.base import TextEditBaseWidget
 from spyder.plugins.outlineexplorer.languages import PythonCFM
@@ -676,7 +676,7 @@ def __init__(self, parent=None):
 
         # re-use parent of completion_widget (usually the main window)
         completion_parent = self.completion_widget.parent()
-        self.kite_call_to_action = KiteCallToAction(self, completion_parent)
+        # self.kite_call_to_action = KiteCallToAction(self, completion_parent)
 
         # Some events should not be triggered during undo/redo
         # such as line stripping
@@ -1153,7 +1153,7 @@ def start_completion_services(self):
             additional_msg = ""
             self.document_did_open()
 
-        logger.debug(u"Completion services available for{0}: {1}".format(
+        logger.debug(u"Completion services available for {0}: {1}".format(
             additional_msg, self.filename))
 
     def register_completion_capabilities(self, capabilities):
@@ -1207,7 +1207,7 @@ def stop_completion_services(self):
         logger.debug('Stopping completion services for %s' % self.filename)
         self.completions_available = False
 
-    @request(method=LSPRequestTypes.DOCUMENT_DID_OPEN, requires_response=False)
+    @request(method=CompletionRequestTypes.DOCUMENT_DID_OPEN, requires_response=False)
     def document_did_open(self):
         """Send textDocument/didOpen request to the server."""
         cursor = self.textCursor()
@@ -1228,7 +1228,7 @@ def document_did_open(self):
         return params
 
     # ------------- LSP: Symbols ---------------------------------------
-    @request(method=LSPRequestTypes.DOCUMENT_SYMBOL)
+    @request(method=CompletionRequestTypes.DOCUMENT_SYMBOL)
     def request_symbols(self):
         """Request document symbols."""
         if not self.document_symbols_enabled:
@@ -1238,7 +1238,7 @@ def request_symbols(self):
         params = {'file': self.filename}
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_SYMBOL)
+    @handles(CompletionRequestTypes.DOCUMENT_SYMBOL)
     def process_symbols(self, params):
         """Handle symbols response."""
         try:
@@ -1256,7 +1256,7 @@ def process_symbols(self, params):
 
     # ------------- LSP: Linting ---------------------------------------
     @request(
-        method=LSPRequestTypes.DOCUMENT_DID_CHANGE, requires_response=False)
+        method=CompletionRequestTypes.DOCUMENT_DID_CHANGE, requires_response=False)
     def document_did_change(self, text=None):
         """Send textDocument/didChange request to the server."""
         self.text_version += 1
@@ -1278,7 +1278,7 @@ def document_did_change(self, text=None):
         }
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS)
+    @handles(CompletionRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS)
     def process_diagnostics(self, params):
         """Handle linting response."""
         try:
@@ -1303,7 +1303,7 @@ def process_diagnostics(self, params):
             self.log_lsp_handle_errors("Error when processing linting")
 
     # ------------- LSP: Completion ---------------------------------------
-    @request(method=LSPRequestTypes.DOCUMENT_COMPLETION)
+    @request(method=CompletionRequestTypes.DOCUMENT_COMPLETION)
     def do_completion(self, automatic=False):
         """Trigger completion."""
         cursor = self.textCursor()
@@ -1324,7 +1324,7 @@ def do_completion(self, automatic=False):
         self.completion_args = (self.textCursor().position(), automatic)
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_COMPLETION)
+    @handles(CompletionRequestTypes.DOCUMENT_COMPLETION)
     def process_completion(self, params):
         """Handle completion response."""
         args = self.completion_args
@@ -1408,17 +1408,17 @@ def sort_key(completion):
             self.completion_widget.show_list(
                 completion_list, position, automatic)
 
-            self.kite_call_to_action.handle_processed_completions(completions)
+            # self.kite_call_to_action.handle_processed_completions(completions)
         except RuntimeError:
             # This is triggered when a codeeditor instance was removed
             # before the response can be processed.
-            self.kite_call_to_action.hide_coverage_cta()
+            # self.kite_call_to_action.hide_coverage_cta()
             return
         except Exception:
             self.log_lsp_handle_errors('Error when processing completions')
 
     # ------------- LSP: Signature Hints ------------------------------------
-    @request(method=LSPRequestTypes.DOCUMENT_SIGNATURE)
+    @request(method=CompletionRequestTypes.DOCUMENT_SIGNATURE)
     def request_signature(self):
         """Ask for signature."""
         self.document_did_change('')
@@ -1432,7 +1432,7 @@ def request_signature(self):
         }
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_SIGNATURE)
+    @handles(CompletionRequestTypes.DOCUMENT_SIGNATURE)
     def process_signatures(self, params):
         """Handle signature response."""
         try:
@@ -1472,7 +1472,7 @@ def process_signatures(self, params):
             self.log_lsp_handle_errors("Error when processing signature")
 
     # ------------- LSP: Hover/Mouse ---------------------------------------
-    @request(method=LSPRequestTypes.DOCUMENT_CURSOR_EVENT)
+    @request(method=CompletionRequestTypes.DOCUMENT_CURSOR_EVENT)
     def request_cursor_event(self):
         text = self.toPlainText()
         cursor = self.textCursor()
@@ -1486,7 +1486,7 @@ def request_cursor_event(self):
         }
         return params
 
-    @request(method=LSPRequestTypes.DOCUMENT_HOVER)
+    @request(method=CompletionRequestTypes.DOCUMENT_HOVER)
     def request_hover(self, line, col, offset, show_hint=True, clicked=True):
         """Request hover information."""
         params = {
@@ -1499,7 +1499,7 @@ def request_hover(self, line, col, offset, show_hint=True, clicked=True):
         self._request_hover_clicked = clicked
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_HOVER)
+    @handles(CompletionRequestTypes.DOCUMENT_HOVER)
     def handle_hover_response(self, contents):
         """Handle hover response."""
         if running_under_pytest():
@@ -1537,7 +1537,7 @@ def handle_hover_response(self, contents):
 
     # ------------- LSP: Go To Definition ----------------------------
     @Slot()
-    @request(method=LSPRequestTypes.DOCUMENT_DEFINITION)
+    @request(method=CompletionRequestTypes.DOCUMENT_DEFINITION)
     def go_to_definition_from_cursor(self, cursor=None):
         """Go to definition from cursor instance (QTextCursor)."""
         if (not self.go_to_definition_enabled or
@@ -1562,7 +1562,7 @@ def go_to_definition_from_cursor(self, cursor=None):
             }
             return params
 
-    @handles(LSPRequestTypes.DOCUMENT_DEFINITION)
+    @handles(CompletionRequestTypes.DOCUMENT_DEFINITION)
     def handle_go_to_definition(self, position):
         """Handle go to definition response."""
         try:
@@ -1594,7 +1594,7 @@ def format_document_or_range(self):
         else:
             self.format_document()
 
-    @request(method=LSPRequestTypes.DOCUMENT_FORMATTING)
+    @request(method=CompletionRequestTypes.DOCUMENT_FORMATTING)
     def format_document(self):
         if not self.formatting_enabled:
             return
@@ -1622,7 +1622,7 @@ def format_document(self):
 
         return params
 
-    @request(method=LSPRequestTypes.DOCUMENT_RANGE_FORMATTING)
+    @request(method=CompletionRequestTypes.DOCUMENT_RANGE_FORMATTING)
     def format_document_range(self):
         if not self.range_formatting_enabled or not self.has_selected_text():
             return
@@ -1665,7 +1665,7 @@ def format_document_range(self):
 
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_FORMATTING)
+    @handles(CompletionRequestTypes.DOCUMENT_FORMATTING)
     def handle_document_formatting(self, edits):
         try:
             self._apply_document_edits(edits)
@@ -1684,7 +1684,7 @@ def handle_document_formatting(self, edits):
             self.sig_stop_operation_in_progress.emit()
             self.operation_in_progress = False
 
-    @handles(LSPRequestTypes.DOCUMENT_RANGE_FORMATTING)
+    @handles(CompletionRequestTypes.DOCUMENT_RANGE_FORMATTING)
     def handle_document_range_formatting(self, edits):
         try:
             self._apply_document_edits(edits)
@@ -1780,7 +1780,7 @@ def cleanup_folding(self):
         folding_panel = self.panels.get(FoldingPanel)
         folding_panel.folding_regions = {}
 
-    @request(method=LSPRequestTypes.DOCUMENT_FOLDING_RANGE)
+    @request(method=CompletionRequestTypes.DOCUMENT_FOLDING_RANGE)
     def request_folding(self):
         """Request folding."""
         if not self.folding_supported or not self.code_folding:
@@ -1788,7 +1788,7 @@ def request_folding(self):
         params = {'file': self.filename}
         return params
 
-    @handles(LSPRequestTypes.DOCUMENT_FOLDING_RANGE)
+    @handles(CompletionRequestTypes.DOCUMENT_FOLDING_RANGE)
     def handle_folding_range(self, response):
         """Handle folding response."""
         ranges = response['params']
@@ -1854,7 +1854,7 @@ def finish_code_folding(self):
             self.update_whitespace_count(line, column)
 
     # ------------- LSP: Save/close file -----------------------------------
-    @request(method=LSPRequestTypes.DOCUMENT_DID_SAVE,
+    @request(method=CompletionRequestTypes.DOCUMENT_DID_SAVE,
              requires_response=False)
     def notify_save(self):
         """Send save request."""
@@ -1863,7 +1863,7 @@ def notify_save(self):
             params['text'] = self.toPlainText()
         return params
 
-    @request(method=LSPRequestTypes.DOCUMENT_DID_CLOSE,
+    @request(method=CompletionRequestTypes.DOCUMENT_DID_CLOSE,
              requires_response=False)
     def notify_close(self):
         """Send close request."""
@@ -4369,7 +4369,7 @@ def keyPressEvent(self, event):
         event.ignore()
         self.sig_key_pressed.emit(event)
 
-        self.kite_call_to_action.handle_key_press(event)
+        # self.kite_call_to_action.handle_key_press(event)
 
         key = event.key()
         text = to_text_string(event.text())
@@ -5064,7 +5064,7 @@ def leaveEvent(self, event):
     def mousePressEvent(self, event):
         """Override Qt method."""
         self.hide_tooltip()
-        self.kite_call_to_action.handle_mouse_press(event)
+        # self.kite_call_to_action.handle_mouse_press(event)
 
         ctrl = event.modifiers() & Qt.ControlModifier
         alt = event.modifiers() & Qt.AltModifier
diff --git a/spyder/plugins/editor/widgets/completion.py b/spyder/plugins/editor/widgets/completion.py
index 0af6316df1f..390ec66bf67 100644
--- a/spyder/plugins/editor/widgets/completion.py
+++ b/spyder/plugins/editor/widgets/completion.py
@@ -18,8 +18,8 @@
 
 # Local imports
 from spyder.utils import icon_manager as ima
-from spyder.plugins.completion.kite.providers.document import KITE_COMPLETION
-from spyder.plugins.completion.manager.api import CompletionItemKind
+# from spyder.plugins.completion.kite.providers.document import KITE_COMPLETION
+from spyder.plugins.completion.api import CompletionItemKind
 from spyder.py3compat import to_text_string
 from spyder.widgets.helperwidgets import HTMLDelegate
 
@@ -221,12 +221,13 @@ def set_item_display(self, item_widget, item_info, height, width):
         img_height = height - 2
         img_width = img_height * 0.8
 
-        if item_provider == KITE_COMPLETION:
-            kite_height = img_height
-            kite_width = (416.14/526.8) * kite_height
-            icon_provider = ima.get_icon('kite', adjust_for_interface=True)
-            icon_provider = ima.base64_from_icon_obj(
-                icon_provider, kite_width, kite_height)
+        # TODO: Define a way to set a completion icon
+        # if item_provider == KITE_COMPLETION:
+        #     kite_height = img_height
+        #     kite_width = (416.14/526.8) * kite_height
+        #     icon_provider = ima.get_icon('kite', adjust_for_interface=True)
+        #     icon_provider = ima.base64_from_icon_obj(
+        #         icon_provider, kite_width, kite_height)
 
         item_text = self.get_html_item_representation(
             item_label, item_type, icon_provider=icon_provider,
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
index 63bce93a485..157d07a1d8a 100644
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -423,7 +423,7 @@ class EditorStack(QWidget):
     run_cell_in_ipyclient = Signal(str, object, str, bool)
     debug_cell_in_ipyclient = Signal(str, object, str, bool)
     update_plugin_title = Signal()
-    editor_focus_changed = Signal()
+    editor_focus_changed = Signal(str, str)
     zoom_in = Signal()
     zoom_out = Signal()
     zoom_reset = Signal()
@@ -621,7 +621,7 @@ def __init__(self, parent, actions):
         self.analysis_timer.timeout.connect(self.analyze_script)
 
         # Update filename label
-        self.editor_focus_changed.connect(self.update_fname_label)
+        self.editor_focus_changed.connect(lambda x, y: self.update_fname_label)
 
         # Accepting drops
         self.setAcceptDrops(True)
@@ -2401,14 +2401,18 @@ def tab_navigation_mru(self, forward=True):
     def focus_changed(self):
         """Editor focus has changed"""
         fwidget = QApplication.focusWidget()
+        fname = None
+        language = None
         for finfo in self.data:
             if fwidget is finfo.editor:
+                fname = finfo.editor.filename
+                language = finfo.editor.language
                 if finfo.editor.operation_in_progress:
                     self.spinner.start()
                 else:
                     self.spinner.stop()
                 self.refresh()
-        self.editor_focus_changed.emit()
+        self.editor_focus_changed.emit(fname, language)
 
     def _refresh_outlineexplorer(self, index=None, update=True, clear=False):
         """Refresh outline explorer panel"""
diff --git a/spyder/plugins/outlineexplorer/plugin.py b/spyder/plugins/outlineexplorer/plugin.py
index 9021d24d871..7f04bc097f5 100644
--- a/spyder/plugins/outlineexplorer/plugin.py
+++ b/spyder/plugins/outlineexplorer/plugin.py
@@ -86,6 +86,8 @@ def register_plugin(self):
         """Register plugin in Spyder's main window"""
         self.main.restore_scrollbar_position.connect(
                                                self.restore_scrollbar_position)
+        self.main.completions.sig_language_client_available.connect(
+            self.start_symbol_services)
         self.add_dockwidget()
 
     def closing_plugin(self, cancelable=False):
diff --git a/spyder/plugins/outlineexplorer/widgets.py b/spyder/plugins/outlineexplorer/widgets.py
index b4ccbef7d8e..64d383fa5cf 100644
--- a/spyder/plugins/outlineexplorer/widgets.py
+++ b/spyder/plugins/outlineexplorer/widgets.py
@@ -23,8 +23,7 @@
 from spyder.config.manager import CONF
 from spyder.py3compat import to_text_string
 from spyder.utils import icon_manager as ima
-from spyder.plugins.completion.manager.api import (
-    SymbolKind, BLOCK_COMMENT, CELL, SYMBOL_KIND_ICON)
+from spyder.plugins.completion.api import SymbolKind, SYMBOL_KIND_ICON
 from spyder.utils.qthelpers import (create_action, create_toolbutton,
                                     set_item_user_text, create_plugin_layout,
                                     create_waitspinner)
@@ -58,8 +57,8 @@
     SymbolKind.EVENT: _('Event'),
     SymbolKind.OPERATOR: _('Operator'),
     SymbolKind.TYPE_PARAMETER: _('Type parameter'),
-    CELL: _('Cell'),
-    BLOCK_COMMENT: _('Block comment')
+    SymbolKind.CELL: _('Cell'),
+    SymbolKind.BLOCK_COMMENT: _('Block comment')
 }
 
 ICON_CACHE = {}
diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py
index ae4cb2618ff..703f3fa7916 100644
--- a/spyder/plugins/preferences/api.py
+++ b/spyder/plugins/preferences/api.py
@@ -37,6 +37,11 @@
 from spyder.widgets.comboboxes import FileComboBox
 
 
+class BaseConfigTab(QWidget):
+    """Stub class to declare a config page."""
+    pass
+
+
 class ConfigAccessMixin(object):
     """Namespace for methods that access config storage"""
     CONF_SECTION = None
@@ -49,6 +54,10 @@ def get_option(self, option, default=NoDefault, section=None):
         section = self.CONF_SECTION if section is None else section
         return CONF.get(section, option, default)
 
+    def remove_option(option, section=None):
+        section = self.CONF_SECTION if section is None else section
+        CONF.remove_option(section, option)
+
 
 class ConfigPage(QWidget):
     """Base class for configuration page in Preferences"""
@@ -128,7 +137,7 @@ class SpyderConfigPage(ConfigPage, ConfigAccessMixin):
     def __init__(self, parent):
         ConfigPage.__init__(self, parent,
                             apply_callback=lambda:
-                            self.apply_settings(self.changed_options))
+                            self._apply_settings_tabs(self.changed_options))
         self.checkboxes = {}
         self.radiobuttons = {}
         self.lineedits = {}
@@ -145,6 +154,18 @@ def __init__(self, parent):
         self.main = parent.main
         self.tabs = None
 
+    def _apply_settings_tabs(self, options):
+        if self.tabs is not None:
+            for i in range(self.tabs.count()):
+                tab = self.tabs.widget(i)
+                layout = tab.layout()
+                for i in range(layout.count()):
+                    widget = layout.itemAt(i).widget()
+                    if hasattr(widget, 'apply_settings'):
+                        if issubclass(type(widget), BaseConfigTab):
+                            options |= widget.apply_settings()
+        self.apply_settings(options)
+
     def apply_settings(self, options):
         raise NotImplementedError
 
@@ -775,6 +796,7 @@ def add_tab(self, Widget):
         else:
             self.tabs.addTab(self.create_tab(widget),
                              Widget.TITLE)
+        self.load_from_conf()
 
 
 class GeneralConfigPage(SpyderConfigPage):
diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py
index df7f7c8a492..ff13dce29aa 100644
--- a/spyder/plugins/projects/plugin.py
+++ b/spyder/plugins/projects/plugin.py
@@ -40,9 +40,9 @@
 from spyder.plugins.projects.utils.watcher import WorkspaceWatcher
 from spyder.plugins.projects.widgets.explorer import ProjectExplorerWidget
 from spyder.plugins.projects.widgets.projectdialog import ProjectDialog
-from spyder.plugins.completion.manager.api import (
-    LSPRequestTypes, FileChangeType, WorkspaceUpdateKind)
-from spyder.plugins.completion.manager.decorators import (
+from spyder.plugins.completion.api import (
+    CompletionRequestTypes, FileChangeType, WorkspaceUpdateKind)
+from spyder.plugins.completion.decorators import (
     request, handles, class_register)
 
 
@@ -179,6 +179,10 @@ def register_plugin(self):
             ipyconsole.run_script(fname, osp.dirname(fname), '', False, False,
                                   False, True, False))
 
+        lspmgr.sig_language_client_available.connect(
+            lambda settings, language:
+                self.start_workspace_services())
+
         # New project connections. Order matters!
         self.sig_project_loaded.connect(
             lambda path:
@@ -191,7 +195,8 @@ def register_plugin(self):
             lambda v: self.main.set_window_title())
         self.sig_project_loaded.connect(
             functools.partial(lspmgr.project_path_update,
-                              update_kind=WorkspaceUpdateKind.ADDITION))
+                              update_kind=WorkspaceUpdateKind.ADDITION,
+                              instance=self))
         self.sig_project_loaded.connect(
             lambda v: self.main.editor.setup_open_files())
         self.sig_project_loaded.connect(self.update_explorer)
@@ -208,7 +213,8 @@ def register_plugin(self):
             lambda v: self.main.set_window_title())
         self.sig_project_closed.connect(
             functools.partial(lspmgr.project_path_update,
-                              update_kind=WorkspaceUpdateKind.DELETION))
+                              update_kind=WorkspaceUpdateKind.DELETION,
+                              instance=self))
         self.sig_project_closed.connect(
             lambda v: self.main.editor.setup_open_files())
         self.sig_project_closed.connect(
@@ -656,7 +662,7 @@ def handle_response(self, method, params):
             handler(params)
 
     @Slot(str, str, bool)
-    @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
+    @request(method=CompletionRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
              requires_response=False)
     def file_moved(self, src_file, dest_file, is_dir):
         """Notify LSP server about a file that is moved."""
@@ -680,7 +686,7 @@ def file_moved(self, src_file, dest_file, is_dir):
         }
         return params
 
-    @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
+    @request(method=CompletionRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
              requires_response=False)
     @Slot(str, bool)
     def file_created(self, src_file, is_dir):
@@ -696,7 +702,7 @@ def file_created(self, src_file, is_dir):
         }
         return params
 
-    @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
+    @request(method=CompletionRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
              requires_response=False)
     @Slot(str, bool)
     def file_deleted(self, src_file, is_dir):
@@ -712,7 +718,7 @@ def file_deleted(self, src_file, is_dir):
         }
         return params
 
-    @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
+    @request(method=CompletionRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE,
              requires_response=False)
     @Slot(str, bool)
     def file_modified(self, src_file, is_dir):
@@ -728,7 +734,7 @@ def file_modified(self, src_file, is_dir):
         }
         return params
 
-    @request(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE,
+    @request(method=CompletionRequestTypes.WORKSPACE_FOLDERS_CHANGE,
              requires_response=False)
     def notify_project_open(self, path):
         """Notify LSP server about project path availability."""
@@ -739,7 +745,7 @@ def notify_project_open(self, path):
         }
         return params
 
-    @request(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE,
+    @request(method=CompletionRequestTypes.WORKSPACE_FOLDERS_CHANGE,
              requires_response=False)
     def notify_project_close(self, path):
         """Notify LSP server to unregister project path."""
@@ -750,8 +756,8 @@ def notify_project_close(self, path):
         }
         return params
 
-    @handles(LSPRequestTypes.WORKSPACE_APPLY_EDIT)
-    @request(method=LSPRequestTypes.WORKSPACE_APPLY_EDIT,
+    @handles(CompletionRequestTypes.WORKSPACE_APPLY_EDIT)
+    @request(method=CompletionRequestTypes.WORKSPACE_APPLY_EDIT,
              requires_response=False)
     def handle_workspace_edit(self, params):
         """Apply edits to multiple files and notify server about success."""
diff --git a/spyder/plugins/pylint/plugin.py b/spyder/plugins/pylint/plugin.py
index 22edcf234b3..f8cddbd39f2 100644
--- a/spyder/plugins/pylint/plugin.py
+++ b/spyder/plugins/pylint/plugin.py
@@ -82,7 +82,8 @@ def register(self):
 
         # Connect to Editor
         widget.sig_edit_goto_requested.connect(editor.load)
-        editor.sig_editor_focus_changed.connect(self._set_filename)
+        editor.sig_editor_focus_changed.connect(
+            lambda x, y: self._set_filename)
 
         # Connect to projects
         projects = self.get_plugin(Plugins.Projects)


Code review comments:
diff_hunk: @@ -120,3 +150,16 @@ def setup_page(self):
         self.setLayout(layout)
         """
         raise NotImplementedError
+
+    def apply_settings(self) -> OptionSet:
+        """
+        Hook called to manually apply settings that cannot be automatically
+        applied manually.
path: spyder/api/preferences.py
text: @user1:
I think the last `manually` here is unnecessary.

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
+
+    def __init__(self, parent, config):
+        """
+        Main completion provider constructor.
+
+        Parameters
+        ----------
+        parent: spyder.plugins.completion.plugin.CompletionPlugin
+            Instance of the completion plugin that manages this provider
+        config: dict
+            Current provider configuration values, whose keys correspond to
+            the ones defined on `CONF_DEFAULTS` and the values correspond to
+            the current values according to the Spyder configuration.
+        """
+        QObject.__init__(self, parent)
+        self.main = parent
+        self.config = config
+
+    def get_name(self) -> str:
+        """Return a human readable name of the completion provider."""
+        return ''
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        pass
+
+    def send_request(
+            self, language: str, req_type: str, req: dict, req_id: int):
+        """
+        Process completion/introspection request from Spyder.
path: spyder/plugins/completion/api.py
text: @user1:
```suggestion
        Send completion/introspection request from Spyder.
```

I don't know if that's correct, but it seems to.

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
+
+    def __init__(self, parent, config):
+        """
+        Main completion provider constructor.
+
+        Parameters
+        ----------
+        parent: spyder.plugins.completion.plugin.CompletionPlugin
+            Instance of the completion plugin that manages this provider
+        config: dict
+            Current provider configuration values, whose keys correspond to
+            the ones defined on `CONF_DEFAULTS` and the values correspond to
+            the current values according to the Spyder configuration.
+        """
+        QObject.__init__(self, parent)
+        self.main = parent
+        self.config = config
+
+    def get_name(self) -> str:
+        """Return a human readable name of the completion provider."""
+        return ''
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        pass
+
+    def send_request(
+            self, language: str, req_type: str, req: dict, req_id: int):
+        """
+        Process completion/introspection request from Spyder.
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        pass
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send notification to completion server based on Spyder changes.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        """
+        pass
+
+    def broadcast_notification(
+            self, notification_type: str, notification: dict):
+        """
+        Send a broadcast notification across all programming languages.
+
+        Parameters
+        ----------
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.CompletionTypes`
+        req: dict
+            Request body
+            {
+                **kwargs: notification-specific parameters
+            }
+        req_id: int
+            Request identifier for response, None if notification
path: spyder/plugins/completion/api.py
text: @user1:
These args don't correspond to the signature.

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
+
+    def __init__(self, parent, config):
+        """
+        Main completion provider constructor.
+
+        Parameters
+        ----------
+        parent: spyder.plugins.completion.plugin.CompletionPlugin
+            Instance of the completion plugin that manages this provider
+        config: dict
+            Current provider configuration values, whose keys correspond to
+            the ones defined on `CONF_DEFAULTS` and the values correspond to
+            the current values according to the Spyder configuration.
+        """
+        QObject.__init__(self, parent)
+        self.main = parent
+        self.config = config
+
+    def get_name(self) -> str:
+        """Return a human readable name of the completion provider."""
+        return ''
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        pass
+
+    def send_request(
+            self, language: str, req_type: str, req: dict, req_id: int):
+        """
+        Process completion/introspection request from Spyder.
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        pass
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send notification to completion server based on Spyder changes.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        """
+        pass
+
+    def broadcast_notification(
+            self, notification_type: str, notification: dict):
+        """
+        Send a broadcast notification across all programming languages.
+
+        Parameters
+        ----------
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.CompletionTypes`
+        req: dict
+            Request body
+            {
+                **kwargs: notification-specific parameters
+            }
+        req_id: int
+            Request identifier for response, None if notification
+        """
+        pass
+
+    def send_response(self, response: dict, resp_id: int):
+        """
+        Send response for server request.
+
+        Parameters
+        ----------
+        response: dict
+            Response body for server
+            {
+                **kwargs: response-specific keys
+            }
+        resp_id: int
+            Request identifier for response
+        """
+        pass
+
+    def update_configuration(self, config: dict):
+        """
+        Handle completion option configuration updates.
+
+        Parameters
+        ----------
+        conf: dict
+            Dictionary containing the new provider configuration
+        """
+        pass
+
+    def project_path_update(self, project_path: str, update_kind: str,
+                            instance: Any):
+        """
+        Handle project path updates on Spyder.
+
+        Parameters
+        ----------
+        project_path: str
+            Path to the project folder modified
+        update_kind: str
+            Path update kind, one of
+            :class:`spyder.plugins.completion.api.WorkspaceUpdateKind`
+        instance: object
+            Reference to :class:`spyder.plugins.projects.plugin.Projects`
+        """
+        pass
+
+    @Slot(object, object)
+    def python_path_update(self, previous_path, new_path):
+        """
+        Handle Python path updates on Spyder.
+
+        Parameters
+        ----------
+        previous_path: Dict
+            Dictionary containing the previous Python path values
+        new_path: Dict
+            Dictionary containing the current Python path values.
+        """
+        pass
+
+    @Slot()
+    def main_interpreter_changed(self):
+        """Handle changes on the main Python interpreter of Spyder."""
+        pass
+
+    def editor_focus_changed(
+            self, filename: str, language: str):
+        """
+        Handle focus changes across tabs and split editors.
+
+        Parameters
+        ----------
+        filename: str
+            Path to the file currently focused on the editor.
+        language: str
+            Name of the programming language of the currently focused file.
+        """
+        pass
+
+    def start_provider(self, language: str) -> bool:
+        """
+        Start completions/introspection services for a given language.
+
+        Parameters
+        ----------
+        language: str
+            Programming language to start analyzing
+
+        Returns
+        -------
+        bool
+            True if language client could be started, otherwise False.
+        """
+        return False
+
+    def stop_provider(self, language: str):
+        """
+        Stop completions/introspection services for a given language.
+
+        Parameters
+        ----------
+        language: str
+            Programming language to stop analyzing
+        """
+        pass
+
+    def start(self):
path: spyder/plugins/completion/api.py
text: @user1:
Can this method be overridden by providers? If not, it should be part of the private API and start with an underscore.

Meeting note: Move signal to docstring and explain that it needs to be emitted by `start`.

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
+
+    def __init__(self, parent, config):
+        """
+        Main completion provider constructor.
+
+        Parameters
+        ----------
+        parent: spyder.plugins.completion.plugin.CompletionPlugin
+            Instance of the completion plugin that manages this provider
+        config: dict
+            Current provider configuration values, whose keys correspond to
+            the ones defined on `CONF_DEFAULTS` and the values correspond to
+            the current values according to the Spyder configuration.
+        """
+        QObject.__init__(self, parent)
+        self.main = parent
+        self.config = config
+
+    def get_name(self) -> str:
+        """Return a human readable name of the completion provider."""
+        return ''
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        pass
+
+    def send_request(
+            self, language: str, req_type: str, req: dict, req_id: int):
+        """
+        Process completion/introspection request from Spyder.
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        pass
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send notification to completion server based on Spyder changes.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        """
+        pass
+
+    def broadcast_notification(
+            self, notification_type: str, notification: dict):
+        """
+        Send a broadcast notification across all programming languages.
+
+        Parameters
+        ----------
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.CompletionTypes`
+        req: dict
+            Request body
+            {
+                **kwargs: notification-specific parameters
+            }
+        req_id: int
+            Request identifier for response, None if notification
+        """
+        pass
+
+    def send_response(self, response: dict, resp_id: int):
+        """
+        Send response for server request.
+
+        Parameters
+        ----------
+        response: dict
+            Response body for server
+            {
+                **kwargs: response-specific keys
+            }
+        resp_id: int
+            Request identifier for response
+        """
+        pass
+
+    def update_configuration(self, config: dict):
+        """
+        Handle completion option configuration updates.
path: spyder/plugins/completion/api.py
text: @user1:
```suggestion
        Handle completion option configuration updates done through Preferences.
```

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
path: spyder/plugins/completion/api.py
text: @user1:
These keys correspond to the staus bar widget ID's? If that's the case, perhaps we should be more explicit about it here.

diff_hunk: @@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Spyder completion container."""
+
+# Standard library imports
+
+# Third-party imports
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.widgets import PluginMainContainer
+
+
+class CompletionContainer(PluginMainContainer):
+    """Stateless class used to store graphical widgets."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.statusbars = {}
+
+    def setup(self, options=None):
+        pass
+
+    def update_actions(self):
+        pass
+
+    def on_option_update(self, _option, _value):
+        pass
+
+    def show_widget(self, Widget):
+        widget = Widget(self)
+
+        if isinstance(widget, QMessageBox):
+            if hasattr(widget, 'sig_restart_spyder'):
+                widget.sig_restart_spyder.connect(self.sig_restart_requested)
+            widget.exec_()
+
+    def register_statusbars(self, statusbar_map):
path: spyder/plugins/completion/container.py
text: @user1:
register_statusbars -> register_statusbar_widgets

diff_hunk: @@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Spyder completion container."""
+
+# Standard library imports
+
+# Third-party imports
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.widgets import PluginMainContainer
+
+
+class CompletionContainer(PluginMainContainer):
+    """Stateless class used to store graphical widgets."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.statusbars = {}
path: spyder/plugins/completion/container.py
text: @user1:
Let's rename this to `statusbar_widgets` for clarity.

diff_hunk: @@ -33,6 +40,16 @@ def __init__(self, parent: SpyderConfigPage):
         if self.TITLE is None or not isinstance(self.TITLE, str):
             raise ValueError("TITLE must be a str")
 
+    def apply_settings(self) -> OptionSet:
+        """
+        Hook called to manually apply settings that cannot be automatically
+        applied manually.
path: spyder/api/preferences.py
text: @user1:
I think the last `manually` here is unnecessary.

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
path: spyder/plugins/completion/plugin.py
text: @user1:
```suggestion
            except Exception as e:
```

Just to be sure we catch all possible errors.

diff_hunk: @@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Spyder completion container."""
+
+# Standard library imports
+
+# Third-party imports
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.widgets import PluginMainContainer
+
+
+class CompletionContainer(PluginMainContainer):
+    """Stateless class used to store graphical widgets."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.statusbars = {}
+
+    def setup(self, options=None):
+        pass
+
+    def update_actions(self):
+        pass
+
+    def on_option_update(self, _option, _value):
+        pass
+
+    def show_widget(self, Widget):
+        widget = Widget(self)
+
+        if isinstance(widget, QMessageBox):
+            if hasattr(widget, 'sig_restart_spyder'):
+                widget.sig_restart_spyder.connect(self.sig_restart_requested)
+            widget.exec_()
+
+    def register_statusbars(self, statusbar_map):
+        for status_key in statusbar_map:
+            StatusBar = statusbar_map[status_key]
+            self.statusbars[status_key] = StatusBar(self)
+
+    def all_statusbars(self):
path: spyder/plugins/completion/container.py
text: @user1:
all_statusbars -> all_statusbar_widgets

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
path: spyder/plugins/completion/plugin.py
text: @user1:
I think declaring CONF_VERSION is not required because CONF_FILE is False.

Meeting note: Remove CONF_DEFAULTS

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
path: spyder/plugins/completion/plugin.py
text: @user2:
Please move this function and `wrap_set_option`, `wrap_remove_option`, `wrap_create_op` and `wrap_apply_settings` to be proper methods of this class. If they are not required for anything outside it, you can prefix them with a double underscore. You can even make them `@user1`s if they don't depend on any attribute of the class.

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
+
+    def __init__(self, parent, config):
+        """
+        Main completion provider constructor.
+
+        Parameters
+        ----------
+        parent: spyder.plugins.completion.plugin.CompletionPlugin
+            Instance of the completion plugin that manages this provider
+        config: dict
+            Current provider configuration values, whose keys correspond to
+            the ones defined on `CONF_DEFAULTS` and the values correspond to
+            the current values according to the Spyder configuration.
+        """
+        QObject.__init__(self, parent)
+        self.main = parent
+        self.config = config
+
+    def get_name(self) -> str:
+        """Return a human readable name of the completion provider."""
+        return ''
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        pass
+
+    def send_request(
+            self, language: str, req_type: str, req: dict, req_id: int):
+        """
+        Process completion/introspection request from Spyder.
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        pass
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send notification to completion server based on Spyder changes.
+
+        Parameters
+        ----------
+        language: str
+            Programming language for the incoming request
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        """
+        pass
+
+    def broadcast_notification(
+            self, notification_type: str, notification: dict):
+        """
+        Send a broadcast notification across all programming languages.
+
+        Parameters
+        ----------
+        req_type: str
path: spyder/plugins/completion/api.py
text: @user1:
```suggestion
        notification_type: str
```

According to the signature, that should be the right arg name.

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
+                logger.warning('Failed to load completion provider from entry '
+                               f'point {entry_point}')
+                raise e
+
+        # Define configuration page and tabs
+        conf_providers = []
+        conf_tabs = []
+        container = self.get_container()
+
+        for provider_key in self.providers:
+            provider = self.providers[provider_key]['instance']
+            for tab in provider.CONF_TABS:
+                # Add set_option/get_option/remove_option to tab definition
+                setattr(tab, 'get_option', wrap_get_option(provider_key))
+                setattr(tab, 'set_option', wrap_set_option(provider_key))
+                setattr(tab, 'remove_option', wrap_remove_option(provider_key))
+                # Wrap apply_settings to return settings correctly
+                setattr(tab, 'apply_settings',
+                        wrap_apply_settings(tab, provider_key))
+                # Wrap create_* methods to consider provider
+                for name, pos in widget_funcs:
+                    setattr(tab, name, wrap_create_op(name, pos, provider_key))
+            conf_tabs += provider.CONF_TABS
+            # Add defined status bars to container
+            container.register_statusbars(provider.STATUS_BARS)
+            conf_providers.append((provider_key, provider.get_name()))
path: spyder/plugins/completion/plugin.py
text: @user1:
Please try to move this to a method (it's too long to be insider another method).

diff_hunk: @@ -778,38 +648,464 @@ class WorkspaceUpdateKind:
     DELETION = 'deletion'
 
 
-# ----------------------- Symbol kind icon names --------------------
-# Additional symbol constants (non-standard)
-BLOCK_COMMENT = 224
-CELL = 225
+# ---------------- OTHER GENERAL PURPOSE CONSTANTS ------------------
+COMPLETION_ENTRYPOINT = 'spyder.completions'
 
-SYMBOL_KIND_ICON = {
-    SymbolKind.FILE: 'file',
-    SymbolKind.MODULE: 'module',
-    SymbolKind.NAMESPACE: 'namespace',
-    SymbolKind.PACKAGE: 'package',
-    SymbolKind.CLASS: 'class',
-    SymbolKind.METHOD: 'method',
-    SymbolKind.PROPERTY: 'property',
-    SymbolKind.FIELD: 'field',
-    SymbolKind.CONSTRUCTOR: 'constructor',
-    SymbolKind.ENUM: 'enum',
-    SymbolKind.INTERFACE: 'interface',
-    SymbolKind.FUNCTION: 'function',
-    SymbolKind.VARIABLE: 'variable',
-    SymbolKind.CONSTANT: 'constant',
-    SymbolKind.STRING: 'string',
-    SymbolKind.NUMBER: 'number',
-    SymbolKind.BOOLEAN: 'boolean',
-    SymbolKind.ARRAY: 'array',
-    SymbolKind.OBJECT: 'object',
-    SymbolKind.KEY: 'key',
-    SymbolKind.NULL: 'null',
-    SymbolKind.ENUM_MEMBER: 'enum_member',
-    SymbolKind.STRUCT: 'struct',
-    SymbolKind.EVENT: 'event',
-    SymbolKind.OPERATOR: 'operator',
-    SymbolKind.TYPE_PARAMETER: 'type_parameter',
-    BLOCK_COMMENT: 'blockcomment',
-    CELL: 'cell'
-}
+# -------------- SPYDER COMPLETION PROVIDER INTERFACE ---------------
+
+class SpyderCompletionProvider(QObject):
+    """
+    Spyder plugin API for completion providers.
+
+    All completion providers must implement this interface in order to interact
+    with Spyder CodeEditor and Projects manager.
+    """
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to send a response back to the completion manager.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that the completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion capabilities are supported
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    sig_show_widget = Signal(object)
+    """
+    This signal is used to display a graphical widget such as a QMessageBox.
+
+    Parameters
+    ----------
+    widget: Union[QWidget, Callable[[QWidget], QWidget]]
+        Widget to display, its constructor should receive parent as first and
+        only argument.
+    """
+
+    sig_update_statusbar = Signal(str, str, tuple, dict)
+    """
+    This signal is used to call a remote method on a statusbar registered via
+    the `STATUS_BAR` attribute.
+
+    Parameters
+    ----------
+    statusbar_key: str
+        Status bar key identifier that was registered on the `STATUS_BAR`
+        attribute.
+    method_name: str
+        Name of the remote method defined on the statusbar.
+    args: tuple
+        Tuple with positional arguments to invoke the method.
+    kwargs: dict
+        Dictionary containing optional arguments to invoke the method.
+    """
+
+    sig_exception_occurred = Signal(dict)
+    """
+    This signal can be emitted to report an exception from any plugin.
+
+    Parameters
+    ----------
+    error_data: dict
+        The dictionary containing error data. The expected keys are:
+        >>> error_data= {
+            "text": str,
+            "is_traceback": bool,
+            "repo": str,
+            "title": str,
+            "label": str,
+            "steps": str,
+        }
+
+    Notes
+    -----
+    The `is_traceback` key indicates if `text` contains plain text or a
+    Python error traceback.
+
+    The `title` and `repo` keys indicate how the error data should
+    customize the report dialog and Github error submission.
+
+    The `label` and `steps` keys allow customizing the content of the
+    error dialog.
+
+    This signal is automatically connected to the main Spyder interface.
+    """
+
+    # ---------------------------- ATTRIBUTES ---------------------------------
+
+    # Name of the completion service
+    # Status: Required
+    COMPLETION_CLIENT_NAME = None
+
+    # Define the priority of this provider, with 1 being the highest one
+    # Status: Required
+    DEFAULT_ORDER = -1
+
+    # Define configuration defaults if using a separate file.
+    # List of tuples, with the first item in the tuple being the section
+    # name and the second item being the default options dictionary.
+    #
+    # CONF_DEFAULTS_EXAMPLE = [
+    #     ('section-name', {'option-1': 'some-value',
+    #                       'option-2': True,}),
+    #     ('another-section-name', {'option-3': 'some-other-value',
+    #                               'option-4': [1, 2, 3],}),
+    # ]
+    CONF_DEFAULTS = []
+
+    # IMPORTANT NOTES:
+    # 1. If you want to *change* the default value of a current option, you
+    #    need to do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
+    # 2. If you want to *remove* options that are no longer needed or if you
+    #    want to *rename* options, then you need to do a MAJOR update in
+    #    version, e.g. from 3.0.0 to 4.0.0
+    # 3. You don't need to touch this value if you're just adding a new option
+    CONF_VERSION = "0.1.0"
+
+    # Widget to be used as entry in Spyder Preferences dialog.
+    CONF_TABS = []
+
+    # A map of status bars that the provider declares to display on Spyder.
+    # Each status bar should correspond to a
+    # :class:`spyder.api.widgets.status.StatusBarWidget` or
+    # a closure that returns a StatusBarWidget.
+    #
+    # STATUS_BAR = {
+    #     'statusbar_key1': StatusBarClass1,
+    #     'statusbar_key2': StatusBarClass2,
+    #     ...
+    # }
+    STATUS_BARS = {}
path: spyder/plugins/completion/api.py
text: @user1:
Let's rename this to STATUS_BAR_CLASSES

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
+                logger.warning('Failed to load completion provider from entry '
+                               f'point {entry_point}')
+                raise e
+
+        # Define configuration page and tabs
+        conf_providers = []
+        conf_tabs = []
+        container = self.get_container()
+
+        for provider_key in self.providers:
+            provider = self.providers[provider_key]['instance']
+            for tab in provider.CONF_TABS:
+                # Add set_option/get_option/remove_option to tab definition
+                setattr(tab, 'get_option', wrap_get_option(provider_key))
+                setattr(tab, 'set_option', wrap_set_option(provider_key))
+                setattr(tab, 'remove_option', wrap_remove_option(provider_key))
+                # Wrap apply_settings to return settings correctly
+                setattr(tab, 'apply_settings',
+                        wrap_apply_settings(tab, provider_key))
+                # Wrap create_* methods to consider provider
+                for name, pos in widget_funcs:
+                    setattr(tab, name, wrap_create_op(name, pos, provider_key))
+            conf_tabs += provider.CONF_TABS
+            # Add defined status bars to container
+            container.register_statusbars(provider.STATUS_BARS)
+            conf_providers.append((provider_key, provider.get_name()))
+
+        self.CONF_WIDGET_CLASS = partialclass(
+            CompletionConfigPage, providers=conf_providers)
+        self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs}
+
+    # ---------------- Public Spyder API required methods ---------------------
+    def get_name(self) -> str:
+        return _('Completion and linting')
+
+    def get_description(self) -> str:
+        return _('This plugin is on charge of handling and dispatching, as '
+                 'well of receiving the responses of completion and linting '
+                 'requests sent to multiple providers.')
+
+    def get_icon(self):
+        return self.create_icon('lspserver')
+
+    def register(self):
+        """Start all available completion providers."""
+        preferences = self.get_plugin(Plugins.Preferences)
+        preferences.register_plugin_preferences(self)
+
+        container = self.get_container()
+        statusbar = self.get_plugin(Plugins.StatusBar)
+        if statusbar:
+            for sb in container.all_statusbars():
+                statusbar.add_status_widget(sb, 0)
+
+        # TODO: Move this section to the new API once the migration of
+        # Application is complete
+        if self.main:
+            self.main.sig_pythonpath_changed.connect(
+                self.sig_pythonpath_changed)
+
+        # TODO: Move this section to the new API once the migration of projects
+        # is complete
+        if self.main.projects:
+            pass
+
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.STOPPED:
+                # TODO: Register status bar widgets
+                provider_enabled = self.get_conf_option(
+                    ('enabled_providers', provider_name), True)
+                if provider_enabled:
+                    provider_info['instance'].start()
+
+    def unregister(self):
+        """Stop all running completion providers."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                # TODO: Remove status bar widgets
+                provider_info['instance'].shutdown()
+
+    def on_close(self, cancelable=False) -> bool:
+        """Check if any provider has any pending task before closing."""
+        can_close = False
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_can_close = provider.can_close()
+                can_close |= provider_can_close
+                if provider_can_close:
+                    provider.shutdown()
+        return can_close
+
+    def after_configuration_update(self, options: List[Union[tuple, str]]):
+        providers_to_update = set({})
+        for option in options:
+            if option == 'completions_wait_for_ms':
+                self.wait_for_ms = self.get_conf_option(
+                    'completions_wait_for_ms')
+            elif isinstance(option, tuple):
+                option_name, provider_name, *__ = option
+                if option_name == 'enabled_providers':
+                    provider_status = self.get_conf_option(
+                        ('enabled_providers', provider_name))
+                    if provider_status:
+                        self.start_provider_instance(provider_name)
+                    else:
+                        self.shutdown_provider_instance(provider_name)
+                elif option_name == 'provider_configuration':
+                    providers_to_update |= {provider_name}
+
+        for provider in providers_to_update:
+            self.update_provider_configuration(provider)
+
+    # ---------- Completion provider registering/start/stop methods -----------
+    def _merge_default_configurations(self,
+                                      Provider: SpyderCompletionProvider,
+                                      provider_name: str,
+                                      provider_configurations: dict):
+        provider_defaults = dict(Provider.CONF_DEFAULTS)
+        provider_conf_version = Provider.CONF_VERSION
+        if provider_name not in provider_configurations:
+            # Pick completion provider default configuration options
+            provider_config = {
+                'version': provider_conf_version,
+                'values': provider_defaults,
+                'defaults': provider_defaults,
+            }
+
+            provider_configurations[provider_name] = provider_config
+
+        # Check if there were any version changes between configurations
+        provider_config = provider_configurations[provider_name]
+        provider_conf_version = parse_version(Provider.CONF_VERSION)
+        current_conf_version = parse_version(provider_config['version'])
+
+        current_conf_values = provider_config['values']
+        current_defaults = provider_config['defaults']
+
+        # Check if there are new default values and copy them
+        new_keys = provider_defaults.keys() - current_conf_values.keys()
+        for new_key in new_keys:
+            current_conf_values[new_key] = provider_defaults[new_key]
+            current_defaults[new_key] = provider_defaults[new_key]
+
+        if provider_conf_version > current_conf_version:
+            # Check if default values were changed between versions,
+            # causing an overwrite of the current options
+            preserved_keys = current_defaults.keys() & provider_defaults.keys()
+            for key in preserved_keys:
+                if current_defaults[key] != provider_defaults[key]:
+                    current_defaults[key] = provider_defaults[key]
+                    current_conf_values[key] = provider_defaults[key]
+
+            if provider_conf_version.major != current_conf_version.major:
+                # Check if keys were removed/renamed from the previous defaults
+                deleted_keys = (
+                    current_defaults.keys() - provider_defaults.keys())
+                for key in deleted_keys:
+                    current_defaults.pop(key)
+                    current_conf_values.pop(key)
+
+        return str(provider_conf_version), current_conf_values, provider_defaults
+
+    def _instantiate_and_register_provider(
+            self, Provider: SpyderCompletionProvider):
+        container = self.get_container()
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+
+        provider_name = Provider.COMPLETION_CLIENT_NAME
+        self._available_providers[provider_name] = Provider
+
+        logger.debug("Completion plugin: Registering {0}".format(
+            provider_name))
+
+        # Merge configuration settings between a provider defaults and
+        # the existing ones
+        (provider_conf_version,
+         current_conf_values,
+         provider_defaults) = self._merge_default_configurations(
+             Provider, provider_name, provider_configurations)
+
+        new_provider_config = {
+            'version': provider_conf_version,
+            'values': current_conf_values,
+            'defaults': provider_defaults
+        }
+        provider_configurations[provider_name] = new_provider_config
+
+        # Update provider configurations
+        self.set_conf_option('provider_configuration', provider_configurations)
+
+        # Merge and update source priority order
+        source_priorities = self.get_conf_option('request_priorities')
+        provider_priority = Provider.DEFAULT_ORDER
+
+        for request in COMPLETION_REQUESTS:
+            request_priorities = source_priorities.get(request, {})
+            if provider_name not in request_priorities:
+                request_priorities[provider_name] = provider_priority - 1
+            source_priorities[request] = request_priorities
+
+        self.source_priority = source_priorities
+        self.set_conf_option('request_priorities', source_priorities)
path: spyder/plugins/completion/plugin.py
text: @user1:
I think we can move this to a different method, to make this one shorter.

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
+                logger.warning('Failed to load completion provider from entry '
+                               f'point {entry_point}')
+                raise e
+
+        # Define configuration page and tabs
+        conf_providers = []
+        conf_tabs = []
+        container = self.get_container()
+
+        for provider_key in self.providers:
+            provider = self.providers[provider_key]['instance']
+            for tab in provider.CONF_TABS:
+                # Add set_option/get_option/remove_option to tab definition
+                setattr(tab, 'get_option', wrap_get_option(provider_key))
+                setattr(tab, 'set_option', wrap_set_option(provider_key))
+                setattr(tab, 'remove_option', wrap_remove_option(provider_key))
+                # Wrap apply_settings to return settings correctly
+                setattr(tab, 'apply_settings',
+                        wrap_apply_settings(tab, provider_key))
+                # Wrap create_* methods to consider provider
+                for name, pos in widget_funcs:
+                    setattr(tab, name, wrap_create_op(name, pos, provider_key))
+            conf_tabs += provider.CONF_TABS
+            # Add defined status bars to container
+            container.register_statusbars(provider.STATUS_BARS)
+            conf_providers.append((provider_key, provider.get_name()))
+
+        self.CONF_WIDGET_CLASS = partialclass(
+            CompletionConfigPage, providers=conf_providers)
+        self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs}
+
+    # ---------------- Public Spyder API required methods ---------------------
+    def get_name(self) -> str:
+        return _('Completion and linting')
+
+    def get_description(self) -> str:
+        return _('This plugin is on charge of handling and dispatching, as '
+                 'well of receiving the responses of completion and linting '
+                 'requests sent to multiple providers.')
+
+    def get_icon(self):
+        return self.create_icon('lspserver')
+
+    def register(self):
+        """Start all available completion providers."""
+        preferences = self.get_plugin(Plugins.Preferences)
+        preferences.register_plugin_preferences(self)
+
+        container = self.get_container()
+        statusbar = self.get_plugin(Plugins.StatusBar)
+        if statusbar:
+            for sb in container.all_statusbars():
+                statusbar.add_status_widget(sb, 0)
+
+        # TODO: Move this section to the new API once the migration of
+        # Application is complete
+        if self.main:
+            self.main.sig_pythonpath_changed.connect(
+                self.sig_pythonpath_changed)
+
+        # TODO: Move this section to the new API once the migration of projects
+        # is complete
+        if self.main.projects:
+            pass
+
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.STOPPED:
+                # TODO: Register status bar widgets
+                provider_enabled = self.get_conf_option(
+                    ('enabled_providers', provider_name), True)
+                if provider_enabled:
+                    provider_info['instance'].start()
+
+    def unregister(self):
+        """Stop all running completion providers."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                # TODO: Remove status bar widgets
+                provider_info['instance'].shutdown()
+
+    def on_close(self, cancelable=False) -> bool:
+        """Check if any provider has any pending task before closing."""
+        can_close = False
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_can_close = provider.can_close()
+                can_close |= provider_can_close
+                if provider_can_close:
+                    provider.shutdown()
+        return can_close
+
+    def after_configuration_update(self, options: List[Union[tuple, str]]):
+        providers_to_update = set({})
+        for option in options:
+            if option == 'completions_wait_for_ms':
+                self.wait_for_ms = self.get_conf_option(
+                    'completions_wait_for_ms')
+            elif isinstance(option, tuple):
+                option_name, provider_name, *__ = option
+                if option_name == 'enabled_providers':
+                    provider_status = self.get_conf_option(
+                        ('enabled_providers', provider_name))
+                    if provider_status:
+                        self.start_provider_instance(provider_name)
+                    else:
+                        self.shutdown_provider_instance(provider_name)
+                elif option_name == 'provider_configuration':
+                    providers_to_update |= {provider_name}
+
+        for provider in providers_to_update:
+            self.update_provider_configuration(provider)
+
+    # ---------- Completion provider registering/start/stop methods -----------
+    def _merge_default_configurations(self,
+                                      Provider: SpyderCompletionProvider,
+                                      provider_name: str,
+                                      provider_configurations: dict):
+        provider_defaults = dict(Provider.CONF_DEFAULTS)
+        provider_conf_version = Provider.CONF_VERSION
+        if provider_name not in provider_configurations:
+            # Pick completion provider default configuration options
+            provider_config = {
+                'version': provider_conf_version,
+                'values': provider_defaults,
+                'defaults': provider_defaults,
+            }
+
+            provider_configurations[provider_name] = provider_config
+
+        # Check if there were any version changes between configurations
+        provider_config = provider_configurations[provider_name]
+        provider_conf_version = parse_version(Provider.CONF_VERSION)
+        current_conf_version = parse_version(provider_config['version'])
+
+        current_conf_values = provider_config['values']
+        current_defaults = provider_config['defaults']
+
+        # Check if there are new default values and copy them
+        new_keys = provider_defaults.keys() - current_conf_values.keys()
+        for new_key in new_keys:
+            current_conf_values[new_key] = provider_defaults[new_key]
+            current_defaults[new_key] = provider_defaults[new_key]
+
+        if provider_conf_version > current_conf_version:
+            # Check if default values were changed between versions,
+            # causing an overwrite of the current options
+            preserved_keys = current_defaults.keys() & provider_defaults.keys()
+            for key in preserved_keys:
+                if current_defaults[key] != provider_defaults[key]:
+                    current_defaults[key] = provider_defaults[key]
+                    current_conf_values[key] = provider_defaults[key]
+
+            if provider_conf_version.major != current_conf_version.major:
+                # Check if keys were removed/renamed from the previous defaults
+                deleted_keys = (
+                    current_defaults.keys() - provider_defaults.keys())
+                for key in deleted_keys:
+                    current_defaults.pop(key)
+                    current_conf_values.pop(key)
+
+        return str(provider_conf_version), current_conf_values, provider_defaults
+
+    def _instantiate_and_register_provider(
+            self, Provider: SpyderCompletionProvider):
+        container = self.get_container()
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+
+        provider_name = Provider.COMPLETION_CLIENT_NAME
+        self._available_providers[provider_name] = Provider
+
+        logger.debug("Completion plugin: Registering {0}".format(
+            provider_name))
+
+        # Merge configuration settings between a provider defaults and
+        # the existing ones
+        (provider_conf_version,
+         current_conf_values,
+         provider_defaults) = self._merge_default_configurations(
+             Provider, provider_name, provider_configurations)
+
+        new_provider_config = {
+            'version': provider_conf_version,
+            'values': current_conf_values,
+            'defaults': provider_defaults
+        }
+        provider_configurations[provider_name] = new_provider_config
+
+        # Update provider configurations
+        self.set_conf_option('provider_configuration', provider_configurations)
+
+        # Merge and update source priority order
+        source_priorities = self.get_conf_option('request_priorities')
+        provider_priority = Provider.DEFAULT_ORDER
+
+        for request in COMPLETION_REQUESTS:
+            request_priorities = source_priorities.get(request, {})
+            if provider_name not in request_priorities:
+                request_priorities[provider_name] = provider_priority - 1
+            source_priorities[request] = request_priorities
+
+        self.source_priority = source_priorities
+        self.set_conf_option('request_priorities', source_priorities)
+
+        provider_instance = Provider(self, new_provider_config['values'])
+
+        # Signals
+        provider_instance.sig_provider_ready.connect(self.provider_available)
+        provider_instance.sig_response_ready.connect(self.receive_response)
+        provider_instance.sig_exception_occurred.connect(
+            self.sig_exception_occurred)
+        provider_instance.sig_language_client_available.connect(
+            self.sig_language_client_available)
+        provider_instance.sig_show_widget.connect(
+            container.show_widget
+        )
+        provider_instance.sig_update_statusbar.connect(
+            container.statusbar_rpc)
+
+        self.sig_pythonpath_changed.connect(
+            provider_instance.python_path_update)
+        self.sig_main_interpreter_changed.connect(
+            provider_instance.main_interpreter_changed)
path: spyder/plugins/completion/plugin.py
text: @user1:
And this can also be moved to yet another method.

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
+                logger.warning('Failed to load completion provider from entry '
+                               f'point {entry_point}')
+                raise e
+
+        # Define configuration page and tabs
+        conf_providers = []
+        conf_tabs = []
+        container = self.get_container()
+
+        for provider_key in self.providers:
+            provider = self.providers[provider_key]['instance']
+            for tab in provider.CONF_TABS:
+                # Add set_option/get_option/remove_option to tab definition
+                setattr(tab, 'get_option', wrap_get_option(provider_key))
+                setattr(tab, 'set_option', wrap_set_option(provider_key))
+                setattr(tab, 'remove_option', wrap_remove_option(provider_key))
+                # Wrap apply_settings to return settings correctly
+                setattr(tab, 'apply_settings',
+                        wrap_apply_settings(tab, provider_key))
+                # Wrap create_* methods to consider provider
+                for name, pos in widget_funcs:
+                    setattr(tab, name, wrap_create_op(name, pos, provider_key))
+            conf_tabs += provider.CONF_TABS
+            # Add defined status bars to container
+            container.register_statusbars(provider.STATUS_BARS)
+            conf_providers.append((provider_key, provider.get_name()))
+
+        self.CONF_WIDGET_CLASS = partialclass(
+            CompletionConfigPage, providers=conf_providers)
+        self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs}
+
+    # ---------------- Public Spyder API required methods ---------------------
+    def get_name(self) -> str:
+        return _('Completion and linting')
+
+    def get_description(self) -> str:
+        return _('This plugin is on charge of handling and dispatching, as '
+                 'well of receiving the responses of completion and linting '
+                 'requests sent to multiple providers.')
+
+    def get_icon(self):
+        return self.create_icon('lspserver')
+
+    def register(self):
+        """Start all available completion providers."""
+        preferences = self.get_plugin(Plugins.Preferences)
+        preferences.register_plugin_preferences(self)
+
+        container = self.get_container()
+        statusbar = self.get_plugin(Plugins.StatusBar)
+        if statusbar:
+            for sb in container.all_statusbars():
+                statusbar.add_status_widget(sb, 0)
+
+        # TODO: Move this section to the new API once the migration of
+        # Application is complete
+        if self.main:
+            self.main.sig_pythonpath_changed.connect(
+                self.sig_pythonpath_changed)
+
+        # TODO: Move this section to the new API once the migration of projects
+        # is complete
+        if self.main.projects:
+            pass
+
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.STOPPED:
+                # TODO: Register status bar widgets
+                provider_enabled = self.get_conf_option(
+                    ('enabled_providers', provider_name), True)
+                if provider_enabled:
+                    provider_info['instance'].start()
+
+    def unregister(self):
+        """Stop all running completion providers."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                # TODO: Remove status bar widgets
+                provider_info['instance'].shutdown()
+
+    def on_close(self, cancelable=False) -> bool:
+        """Check if any provider has any pending task before closing."""
+        can_close = False
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_can_close = provider.can_close()
+                can_close |= provider_can_close
+                if provider_can_close:
+                    provider.shutdown()
+        return can_close
+
+    def after_configuration_update(self, options: List[Union[tuple, str]]):
+        providers_to_update = set({})
+        for option in options:
+            if option == 'completions_wait_for_ms':
+                self.wait_for_ms = self.get_conf_option(
+                    'completions_wait_for_ms')
+            elif isinstance(option, tuple):
+                option_name, provider_name, *__ = option
+                if option_name == 'enabled_providers':
+                    provider_status = self.get_conf_option(
+                        ('enabled_providers', provider_name))
+                    if provider_status:
+                        self.start_provider_instance(provider_name)
+                    else:
+                        self.shutdown_provider_instance(provider_name)
+                elif option_name == 'provider_configuration':
+                    providers_to_update |= {provider_name}
+
+        for provider in providers_to_update:
+            self.update_provider_configuration(provider)
+
+    # ---------- Completion provider registering/start/stop methods -----------
+    def _merge_default_configurations(self,
+                                      Provider: SpyderCompletionProvider,
+                                      provider_name: str,
+                                      provider_configurations: dict):
+        provider_defaults = dict(Provider.CONF_DEFAULTS)
+        provider_conf_version = Provider.CONF_VERSION
+        if provider_name not in provider_configurations:
+            # Pick completion provider default configuration options
+            provider_config = {
+                'version': provider_conf_version,
+                'values': provider_defaults,
+                'defaults': provider_defaults,
+            }
+
+            provider_configurations[provider_name] = provider_config
+
+        # Check if there were any version changes between configurations
+        provider_config = provider_configurations[provider_name]
+        provider_conf_version = parse_version(Provider.CONF_VERSION)
+        current_conf_version = parse_version(provider_config['version'])
+
+        current_conf_values = provider_config['values']
+        current_defaults = provider_config['defaults']
+
+        # Check if there are new default values and copy them
+        new_keys = provider_defaults.keys() - current_conf_values.keys()
+        for new_key in new_keys:
+            current_conf_values[new_key] = provider_defaults[new_key]
+            current_defaults[new_key] = provider_defaults[new_key]
+
+        if provider_conf_version > current_conf_version:
+            # Check if default values were changed between versions,
+            # causing an overwrite of the current options
+            preserved_keys = current_defaults.keys() & provider_defaults.keys()
+            for key in preserved_keys:
+                if current_defaults[key] != provider_defaults[key]:
+                    current_defaults[key] = provider_defaults[key]
+                    current_conf_values[key] = provider_defaults[key]
+
+            if provider_conf_version.major != current_conf_version.major:
+                # Check if keys were removed/renamed from the previous defaults
+                deleted_keys = (
+                    current_defaults.keys() - provider_defaults.keys())
+                for key in deleted_keys:
+                    current_defaults.pop(key)
+                    current_conf_values.pop(key)
+
+        return str(provider_conf_version), current_conf_values, provider_defaults
+
+    def _instantiate_and_register_provider(
+            self, Provider: SpyderCompletionProvider):
+        container = self.get_container()
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+
+        provider_name = Provider.COMPLETION_CLIENT_NAME
+        self._available_providers[provider_name] = Provider
+
+        logger.debug("Completion plugin: Registering {0}".format(
+            provider_name))
+
+        # Merge configuration settings between a provider defaults and
+        # the existing ones
+        (provider_conf_version,
+         current_conf_values,
+         provider_defaults) = self._merge_default_configurations(
+             Provider, provider_name, provider_configurations)
+
+        new_provider_config = {
+            'version': provider_conf_version,
+            'values': current_conf_values,
+            'defaults': provider_defaults
+        }
+        provider_configurations[provider_name] = new_provider_config
+
+        # Update provider configurations
+        self.set_conf_option('provider_configuration', provider_configurations)
path: spyder/plugins/completion/plugin.py
text: @user1:
This can also be part of a different method.

diff_hunk: @@ -0,0 +1,941 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Spyder completion plugin.
+
+This plugin is on charge of creating and managing multiple code completion and
+introspection clients.
+"""
+
+# Standard library imports
+from collections import defaultdict
+import functools
+import inspect
+import logging
+from pkg_resources import parse_version, iter_entry_points
+from typing import List, Any, Union, Optional, Tuple
+
+# Third-party imports
+from qtpy.QtCore import QMutex, QMutexLocker, QTimer, Slot, Signal
+from qtpy.QtWidgets import QMessageBox
+
+# Local imports
+from spyder.api.plugins import SpyderPluginV2, Plugins
+from spyder.config.base import _
+from spyder.config.user import NoDefault
+from spyder.plugins.completion.api import (CompletionRequestTypes,
+                                           SpyderCompletionProvider,
+                                           COMPLETION_ENTRYPOINT)
+from spyder.plugins.completion.confpage import CompletionConfigPage
+from spyder.plugins.completion.container import CompletionContainer
+
+logger = logging.getLogger(__name__)
+COMPLETION_REQUESTS = [getattr(CompletionRequestTypes, c)
+                       for c in dir(CompletionRequestTypes) if c.isupper()]
+
+
+def partialclass(cls, *args, **kwds):
+    """Return a partial class constructor."""
+    class NewCls(cls):
+        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)
+    return NewCls
+
+
+class CompletionPlugin(SpyderPluginV2):
+    """
+    Spyder completion plugin.
+
+    This class provides a completion and linting plugin for the editor in
+    Spyder.
+
+    This plugin works by forwarding all the completion/linting requests to a
+    set of :class:`SpyderCompletionProvider` instances that are registered
+    and discovered via entrypoints.
+
+    This plugin can assume that `fallback`, `snippets`, `lsp` and `kite`
+    completion providers are available, since they are included as part of
+    Spyder.
+    """
+
+    NAME = 'completions'
+    CONF_SECTION = 'completions'
+    REQUIRES = [Plugins.Preferences]
+    OPTIONAL = [Plugins.StatusBar]
+
+    CONF_DEFAULTS = [
+        ('enabled_providers', {}),
+        ('provider_configuration', {}),
+        ('request_priorities', {})
+    ]
+    CONF_VERSION = '0.1.0'
+    CONF_FILE = False
+    ADDITIONAL_CONF_TABS = {}
+
+    # The configuration page is created dynamically based on the providers
+    # loaded
+    CONF_WIDGET_CLASS = None
+
+    # Empty container used to store graphical widgets
+    CONTAINER_CLASS = CompletionContainer
+
+    # Ad Hoc polymorphism signals to make this plugin a completion provider
+    # without inheriting from SpyderCompletionProvider
+
+    sig_response_ready = Signal(str, int, dict)
+    """
+    This signal is used to receive a response from a completion provider.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client that produced this response.
+    request_seq: int
+        Sequence number for the request.
+    response: dict
+        Actual request corpus response.
+    """
+
+    sig_provider_ready = Signal(str)
+    """
+    This signal is used to indicate that a completion provider is ready
+    to handle requests.
+
+    Parameters
+    ----------
+    completion_client_name: str
+        Name of the completion client.
+    """
+
+    sig_pythonpath_changed = Signal(object, object)
+    """
+    This signal is used to receive changes on the PythonPath.
+
+    Parameters
+    ----------
+    prev_path: dict
+        Previous PythonPath settings.
+    new_path: dict
+        New PythonPath settings.
+    """
+
+    sig_main_interpreter_changed = Signal()
+    """
+    This signal is used to receive changes on the main Python interpreter.
+    """
+
+    sig_language_client_available = Signal(dict, str)
+    """
+    This signal is used to indicate that completion services are available
+    for a given programming language.
+
+    Parameters
+    ----------
+    completion_capabilites: dict
+        Available configurations supported by the client, it should conform to
+        `spyder.plugins.completion.api.SERVER_CAPABILITES`.
+    language: str
+        Name of the programming language whose completion capabilites are
+        available.
+    """
+
+    # Provider status constants
+    RUNNING = 'running'
+    STOPPED = 'stopped'
+
+    SKIP_INTERMEDIATE_REQUESTS = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    AGGREGATE_RESPONSES = {
+        CompletionRequestTypes.DOCUMENT_COMPLETION
+    }
+
+    def __init__(self, parent, configuration=None):
+        super().__init__(parent, configuration)
+
+        # Available completion providers
+        self._available_providers = {}
+
+        # Instantiated completion providers
+        self.providers = {}
+
+        # Mapping that indicates if there are completion services available
+        # for a given language
+        self.language_status = {}
+
+        # Mapping that contains the ids and the current completion/linting
+        # requests in progress
+        self.requests = {}
+
+        # Current request sequence identifier
+        self.req_id = 0
+
+        # Lock to prevent concurrent access to requests mapping
+        self.collection_mutex = QMutex(QMutex.Recursive)
+
+        # Completion request priority
+        self.source_priority = {}
+
+        # Projects plugin reference
+        self.projects = None
+
+        # Timeout limit for a response to be received
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+
+        # Set/Get option patching definitions
+        def wrap_get_option(provider):
+            plugin = self
+            def wrapper(self, option, default=NoDefault, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.get_conf_option(option, default, section)
+            return wrapper
+
+        def wrap_set_option(provider):
+            plugin = self
+            def wrapper(self, option, value, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                return plugin.set_conf_option(option, value, section)
+            return wrapper
+
+        def wrap_remove_option(provider):
+            plugin = self
+            def wrapper(self, option, section=None):
+                if section is None:
+                    if isinstance(option, tuple):
+                        option = ('provider_configuration', provider, 'values',
+                                  *option)
+                    else:
+                        option = ('provider_configuration', provider, 'values',
+                                  option)
+                    return plugin.remove_conf_option(option, section)
+            return wrapper
+
+        # Wrap create_* methods in configpage
+        def wrap_create_op(create_name, opt_pos, provider):
+            def wrapper(self, *args, **kwargs):
+                if kwargs.get('section', None) is None:
+                    arg_list = list(args)
+                    if isinstance(args[opt_pos], tuple):
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            *args[opt_pos])
+                    else:
+                        arg_list[opt_pos] = (
+                            'provider_configuration', provider, 'values',
+                            args[opt_pos])
+                    args = tuple(arg_list)
+                call = getattr(self.parent, create_name)
+                widget = call(*args, **kwargs)
+                widget.setParent(self)
+                return widget
+            return wrapper
+
+        def wrap_apply_settings(Tab, provider):
+            prev_method = Tab.apply_settings
+            def wrapper(self):
+                wrapped_opts = set({})
+                for opt in prev_method(self):
+                    if isinstance(opt, tuple):
+                        wrapped_opts |= {('provider_configuration',
+                                          provider, 'values', *opt)}
+                    else:
+                        wrapped_opts |= {(
+                            'provider_configuration', provider, 'values', opt)}
+                return wrapped_opts
+            return wrapper
+
+        # Filter widget creation functions in ConfigPage
+        members = inspect.getmembers(CompletionConfigPage)
+        widget_funcs = []
+        for name, call in members:
+            if name.startswith('create_'):
+                sig = inspect.signature(call)
+                parameters = sig.parameters
+                if 'option' in sig.parameters:
+                    pos = -1
+                    for param in parameters:
+                        if param == 'option':
+                            break
+                        pos += 1
+                    widget_funcs.append((name, pos))
+
+        # Find and instantiate all completion providers registered via
+        # entrypoints
+        for entry_point in iter_entry_points(COMPLETION_ENTRYPOINT):
+            try:
+                logger.debug(f'Loading entry point: {entry_point}')
+                Provider = entry_point.resolve()
+                self._instantiate_and_register_provider(Provider)
+            except ImportError as e:
+                logger.warning('Failed to load completion provider from entry '
+                               f'point {entry_point}')
+                raise e
+
+        # Define configuration page and tabs
+        conf_providers = []
+        conf_tabs = []
+        container = self.get_container()
+
+        for provider_key in self.providers:
+            provider = self.providers[provider_key]['instance']
+            for tab in provider.CONF_TABS:
+                # Add set_option/get_option/remove_option to tab definition
+                setattr(tab, 'get_option', wrap_get_option(provider_key))
+                setattr(tab, 'set_option', wrap_set_option(provider_key))
+                setattr(tab, 'remove_option', wrap_remove_option(provider_key))
+                # Wrap apply_settings to return settings correctly
+                setattr(tab, 'apply_settings',
+                        wrap_apply_settings(tab, provider_key))
+                # Wrap create_* methods to consider provider
+                for name, pos in widget_funcs:
+                    setattr(tab, name, wrap_create_op(name, pos, provider_key))
+            conf_tabs += provider.CONF_TABS
+            # Add defined status bars to container
+            container.register_statusbars(provider.STATUS_BARS)
+            conf_providers.append((provider_key, provider.get_name()))
+
+        self.CONF_WIDGET_CLASS = partialclass(
+            CompletionConfigPage, providers=conf_providers)
+        self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs}
+
+    # ---------------- Public Spyder API required methods ---------------------
+    def get_name(self) -> str:
+        return _('Completion and linting')
+
+    def get_description(self) -> str:
+        return _('This plugin is on charge of handling and dispatching, as '
+                 'well of receiving the responses of completion and linting '
+                 'requests sent to multiple providers.')
+
+    def get_icon(self):
+        return self.create_icon('lspserver')
+
+    def register(self):
+        """Start all available completion providers."""
+        preferences = self.get_plugin(Plugins.Preferences)
+        preferences.register_plugin_preferences(self)
+
+        container = self.get_container()
+        statusbar = self.get_plugin(Plugins.StatusBar)
+        if statusbar:
+            for sb in container.all_statusbars():
+                statusbar.add_status_widget(sb, 0)
+
+        # TODO: Move this section to the new API once the migration of
+        # Application is complete
+        if self.main:
+            self.main.sig_pythonpath_changed.connect(
+                self.sig_pythonpath_changed)
+
+        # TODO: Move this section to the new API once the migration of projects
+        # is complete
+        if self.main.projects:
+            pass
+
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.STOPPED:
+                # TODO: Register status bar widgets
+                provider_enabled = self.get_conf_option(
+                    ('enabled_providers', provider_name), True)
+                if provider_enabled:
+                    provider_info['instance'].start()
+
+    def unregister(self):
+        """Stop all running completion providers."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                # TODO: Remove status bar widgets
+                provider_info['instance'].shutdown()
+
+    def on_close(self, cancelable=False) -> bool:
+        """Check if any provider has any pending task before closing."""
+        can_close = False
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_can_close = provider.can_close()
+                can_close |= provider_can_close
+                if provider_can_close:
+                    provider.shutdown()
+        return can_close
+
+    def after_configuration_update(self, options: List[Union[tuple, str]]):
+        providers_to_update = set({})
+        for option in options:
+            if option == 'completions_wait_for_ms':
+                self.wait_for_ms = self.get_conf_option(
+                    'completions_wait_for_ms')
+            elif isinstance(option, tuple):
+                option_name, provider_name, *__ = option
+                if option_name == 'enabled_providers':
+                    provider_status = self.get_conf_option(
+                        ('enabled_providers', provider_name))
+                    if provider_status:
+                        self.start_provider_instance(provider_name)
+                    else:
+                        self.shutdown_provider_instance(provider_name)
+                elif option_name == 'provider_configuration':
+                    providers_to_update |= {provider_name}
+
+        for provider in providers_to_update:
+            self.update_provider_configuration(provider)
+
+    # ---------- Completion provider registering/start/stop methods -----------
+    def _merge_default_configurations(self,
+                                      Provider: SpyderCompletionProvider,
+                                      provider_name: str,
+                                      provider_configurations: dict):
+        provider_defaults = dict(Provider.CONF_DEFAULTS)
+        provider_conf_version = Provider.CONF_VERSION
+        if provider_name not in provider_configurations:
+            # Pick completion provider default configuration options
+            provider_config = {
+                'version': provider_conf_version,
+                'values': provider_defaults,
+                'defaults': provider_defaults,
+            }
+
+            provider_configurations[provider_name] = provider_config
+
+        # Check if there were any version changes between configurations
+        provider_config = provider_configurations[provider_name]
+        provider_conf_version = parse_version(Provider.CONF_VERSION)
+        current_conf_version = parse_version(provider_config['version'])
+
+        current_conf_values = provider_config['values']
+        current_defaults = provider_config['defaults']
+
+        # Check if there are new default values and copy them
+        new_keys = provider_defaults.keys() - current_conf_values.keys()
+        for new_key in new_keys:
+            current_conf_values[new_key] = provider_defaults[new_key]
+            current_defaults[new_key] = provider_defaults[new_key]
+
+        if provider_conf_version > current_conf_version:
+            # Check if default values were changed between versions,
+            # causing an overwrite of the current options
+            preserved_keys = current_defaults.keys() & provider_defaults.keys()
+            for key in preserved_keys:
+                if current_defaults[key] != provider_defaults[key]:
+                    current_defaults[key] = provider_defaults[key]
+                    current_conf_values[key] = provider_defaults[key]
+
+            if provider_conf_version.major != current_conf_version.major:
+                # Check if keys were removed/renamed from the previous defaults
+                deleted_keys = (
+                    current_defaults.keys() - provider_defaults.keys())
+                for key in deleted_keys:
+                    current_defaults.pop(key)
+                    current_conf_values.pop(key)
+
+        return str(provider_conf_version), current_conf_values, provider_defaults
+
+    def _instantiate_and_register_provider(
+            self, Provider: SpyderCompletionProvider):
+        container = self.get_container()
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+
+        provider_name = Provider.COMPLETION_CLIENT_NAME
+        self._available_providers[provider_name] = Provider
+
+        logger.debug("Completion plugin: Registering {0}".format(
+            provider_name))
+
+        # Merge configuration settings between a provider defaults and
+        # the existing ones
+        (provider_conf_version,
+         current_conf_values,
+         provider_defaults) = self._merge_default_configurations(
+             Provider, provider_name, provider_configurations)
+
+        new_provider_config = {
+            'version': provider_conf_version,
+            'values': current_conf_values,
+            'defaults': provider_defaults
+        }
+        provider_configurations[provider_name] = new_provider_config
+
+        # Update provider configurations
+        self.set_conf_option('provider_configuration', provider_configurations)
+
+        # Merge and update source priority order
+        source_priorities = self.get_conf_option('request_priorities')
+        provider_priority = Provider.DEFAULT_ORDER
+
+        for request in COMPLETION_REQUESTS:
+            request_priorities = source_priorities.get(request, {})
+            if provider_name not in request_priorities:
+                request_priorities[provider_name] = provider_priority - 1
+            source_priorities[request] = request_priorities
+
+        self.source_priority = source_priorities
+        self.set_conf_option('request_priorities', source_priorities)
+
+        provider_instance = Provider(self, new_provider_config['values'])
+
+        # Signals
+        provider_instance.sig_provider_ready.connect(self.provider_available)
+        provider_instance.sig_response_ready.connect(self.receive_response)
+        provider_instance.sig_exception_occurred.connect(
+            self.sig_exception_occurred)
+        provider_instance.sig_language_client_available.connect(
+            self.sig_language_client_available)
+        provider_instance.sig_show_widget.connect(
+            container.show_widget
+        )
+        provider_instance.sig_update_statusbar.connect(
+            container.statusbar_rpc)
+
+        self.sig_pythonpath_changed.connect(
+            provider_instance.python_path_update)
+        self.sig_main_interpreter_changed.connect(
+            provider_instance.main_interpreter_changed)
+
+        self.providers[provider_name] = {
+            'instance': provider_instance,
+            'status': self.STOPPED
+        }
+
+        for language in self.language_status:
+            server_status = self.language_status[language]
+            server_status[provider_name] = False
+
+    @Slot(str)
+    def provider_available(self, provider_name: str):
+        """Indicate that the completion provider `provider_name` is running."""
+        provider_info = self.providers[provider_name]
+        provider_info['status'] = self.RUNNING
+        self.sig_provider_ready.emit(provider_name)
+
+    def start_provider(self, language: str) -> bool:
+        """Start completion providers for a given programming language."""
+        started = False
+        language_providers = self.language_status.get(language, {})
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider = provider_info['instance']
+                provider_started = provider.start_provider(language)
+                started |= provider_started
+                language_providers[provider_name] = provider_started
+        self.language_status[language] = language_providers
+        return started
+
+    def stop_provider(self, language: str):
+        """Stop completion providers for a given programming language."""
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].stop_provider(language)
+        self.language_status.pop(language)
+
+    def get_provider(self, name: str) -> SpyderCompletionProvider:
+        """Get the :class:`SpyderCompletionProvider` identified with `name`."""
+        return self.providers[name]['instance']
+
+    def is_provider_running(self, name: str) -> bool:
+        # TODO: Let LSP server emit sig_provider_ready
+        # if name == LanguageServerPlugin.COMPLETION_CLIENT_NAME:
+        #     # The LSP plugin does not emit a plugin ready signal
+        #     return name in self.clients
+
+        status = self.clients.get(name, {}).get('status', self.STOPPED)
+        return status == self.RUNNING
+
+    def available_providers_for_language(self, language: str) -> List[str]:
+        providers = []
+        if language in self.language_status:
+            provider_status = self.language_status[language]
+            providers = [p for p in provider_status if provider_status[p]]
+        return providers
+
+    def sort_providers_for_request(
+            self, providers: List[str], req_type: str) -> List[str]:
+        request_order = self.source_priority[req_type]
+        return sorted(providers, key=lambda p: request_order[p])
+
+    def start_provider_instance(self, provider_name: str):
+        """Start a given provider."""
+        provider_info = self.providers[provider_name]
+        if provider_info['status'] == self.STOPPED:
+            provider_info['instance'].start()
+
+    def shutdown_provider_instance(self, provider_name: str):
+        """Shutdown a given provider."""
+        provider_info = self.providers[provider_name]
+        if provider_info['status'] == self.RUNNING:
+            provider_info['instance'].shutdown()
+            provider_info['status'] = self.STOPPED
+
+    def update_provider_configuration(self, provider_name: str):
+        """Update a provider configuration."""
+        provider_configurations = self.get_conf_option(
+            'provider_configuration')
+        provider_info = self.providers[provider_name]
+        if provider_info['status'] == self.RUNNING:
+            provider_configuration = provider_configurations[provider_name]
+            config = provider_configuration['values']
+            provider_info['instance'].update_configuration(config)
+
+    # --------------- Public completion API request methods -------------------
+    def send_request(self, language: str, req_type: str, req: dict):
+        """
+        Send a completion or linting request to all available providers.
+
+        The completion request `req_type` needs to have a response.
+
+        Parameters
+        ----------
+        language: str
+            Name of the programming language of the file that emits the
+            request.
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: request-specific parameters
+            }
+        req_id: int
+            Request identifier for response
+        """
+        req_id = self.req_id
+        self.req_id += 1
+
+        self.requests[req_id] = {
+            'language': language,
+            'req_type': req_type,
+            'response_instance': req['response_instance'],
+            'sources': {},
+            'timed_out': False,
+        }
+
+        # Start the timer on this request
+        if req_type in self.AGGREGATE_RESPONSES:
+            if self.wait_for_ms > 0:
+                QTimer.singleShot(self.wait_for_ms,
+                                  lambda: self.receive_timeout(req_id))
+            else:
+                self.requests[req_id]['timed_out'] = True
+
+        # Send request to all running completion providers
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            provider_info['instance'].send_request(
+                language, req_type, req, req_id)
+
+    def send_notification(
+            self, language: str, notification_type: str, notification: dict):
+        """
+        Send a notification to all available completion providers.
+
+        Parameters
+        ----------
+        language: str
+            Name of the programming language of the file that emits the
+            request.
+        notification_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        notification: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: notification-specific parameters
+            }
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].send_notification(
+                    language, notification_type, notification)
+
+    def broadcast_notification(self, req_type: str, req: dict):
+        """
+        Send a notification to all available completion providers for all
+        programming languages.
+
+        Parameters
+        ----------
+        req_type: str
+            Type of request, one of
+            :class:`spyder.plugins.completion.api.CompletionRequestTypes`
+        req: dict
+            Request body
+            {
+                'filename': str,
+                **kwargs: notification-specific parameters
+            }
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].broadcast_notification(
+                    req_type, req)
+
+    def project_path_update(self, project_path: str, update_kind='addition',
+                            instance=None):
+        """
+        Handle project path updates on Spyder.
+
+        Parameters
+        ----------
+        project_path: str
+            Path to the project folder modified
+        update_kind: str
+            Path update kind, one of
+            :class:`spyder.plugins.completion.WorkspaceUpdateKind`
+        instance: object
+            Reference to :class:`spyder.plugins.projects.plugin.Projects`
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].project_path_update(
+                    project_path, update_kind, instance
+                )
+
+    @Slot(str, str)
+    def editor_focus_changed(
+            self, filename: Optional[str], language: Optional[str]):
+        """
+        Handle focus changes across tabs and split editors.
+
+        Parameters
+        ----------
+        filename: Optional[str]
+            Path to the file currently focused on the editor. If None, then
+            the focus is not currently on the editor.
+        language: Optional[str]
+            Name of the programming language of the currently focused file.
+            If None, then the focus is not currently on the editor.
+        """
+        if filename is not None and language is not None:
+            for provider_name in self.providers:
+                provider_info = self.providers[provider_name]
+                if provider_info['status'] == self.RUNNING:
+                    provider_info['instance'].editor_focus_changed(
+                        filename, language)
+
+    def register_file(self, language: str, filename: str, codeeditor):
+        """
+        Register file to perform completions.
+        If a language client is not available for a given file, then this
+        method should keep a queue, such that files can be initialized once
+        a server is available.
+
+        Parameters
+        ----------
+        language: str
+            Programming language of the given file
+        filename: str
+            Filename to register
+        codeeditor: spyder.plugins.editor.widgets.codeeditor.CodeEditor
+            Codeeditor to send the client configurations
+        """
+        for provider_name in self.providers:
+            provider_info = self.providers[provider_name]
+            if provider_info['status'] == self.RUNNING:
+                provider_info['instance'].register_file(
+                    language, filename, codeeditor
+                )
+
+    def update_configuration(self):
+        """Handle completion option configuration updates."""
+        self.wait_for_ms = self.get_conf_option('completions_wait_for_ms')
+        for provider_name in self.providers:
+            self.update_provider_configuration(provider_name)
+
+    def get_global_option(
+            self, option: str, section: str, default: Any = None) -> Any:
+        """
+        Retrieve an option value from the global Spyder configurations.
+
+        Parameters
+        ----------
+        option: str
+            Option name to lookup for in global Spyder configurations.
+        default: Any
+            Default value to return if `option_name` was not found.
+        section: str
+            Name of the configuration section in Spyder
+
+        Returns
+        -------
+        Any
+            Either the default value if `option_name` was not found on the
+            settings or the actual value stored.
+        """
+        return self.get_conf_option(option, default=default, section=section)
+
+    # ----------------- Completion result processing methods ------------------
+    @Slot(str, int, dict)
+    def receive_response(
+            self, completion_source: str, req_id: int, resp: dict):
+        """Process request response from a completion provider."""
+        logger.debug("Completion plugin: Request {0} Got response "
+                     "from {1}".format(req_id, completion_source))
+
+        if req_id not in self.requests:
+            return
+
+        with QMutexLocker(self.collection_mutex):
+            request_responses = self.requests[req_id]
+            request_responses['sources'][completion_source] = resp
+            self.match_and_reply(req_id)
+
+    @Slot(int)
+    def receive_timeout(self, req_id: int):
+        """Collect all provider completions and reply on timeout."""
+        # On timeout, collect all completions and return to the user
+        if req_id not in self.requests:
+            return
+
+        logger.debug("Completion plugin: Request {} timed out".format(req_id))
+
+        with QMutexLocker(self.collection_mutex):
+            request_responses = self.requests[req_id]
+            request_responses['timed_out'] = True
+            self.match_and_reply(req_id)
+
+    def match_and_reply(self, req_id: int):
+        """
+        Decide how to send the responses corresponding to req_id to
+        the instance that requested them.
+        """
+        if req_id not in self.requests:
+            return
+        request_responses = self.requests[req_id]
+        language = request_responses['language'].lower()
+        req_type = request_responses['req_type']
+
+        if req_type in self.AGGREGATE_RESPONSES:
+            # Wait only for the available providers for the given request
+            available_providers = self.available_providers_for_language(
+                language)
+            sorted_providers = self.sort_providers_for_request(
+                available_providers, req_type)
+            timed_out = request_responses['timed_out']
+            all_returned = all(source in request_responses['sources']
+                            for source in sorted_providers)
+            if not timed_out:
+                # Before the timeout
+                if all_returned:
+                    self.skip_and_reply(req_id)
+            else:
+                # After the timeout
+                any_nonempty = any(request_responses['sources'].get(source)
+                                for source in sorted_providers)
+                if all_returned or any_nonempty:
+                    self.skip_and_reply(req_id)
+        else:
+            self.skip_and_reply(req_id)
+
+    def skip_and_reply(self, req_id: int):
+        """
+        Skip intermediate responses coming from the same CodeEditor
+        instance for some types of requests, and send the last one to
+        it.
+        """
+        request_responses = self.requests[req_id]
+        req_type = request_responses['req_type']
+        response_instance = id(request_responses['response_instance'])
+        do_send = True
+
+        # This is necessary to prevent sending completions for old requests
+        # See spyder-ide/spyder#10798
+        if req_type in self.SKIP_INTERMEDIATE_REQUESTS:
+            max_req_id = max(
+                [key for key, item in self.requests.items()
+                 if item['req_type'] == req_type
+                 and id(item['response_instance']) == response_instance]
+                or [-1])
+            do_send = (req_id == max_req_id)
+
+        logger.debug("Completion plugin: Request {} removed".format(req_id))
+        del self.requests[req_id]
+
+        # Send only recent responses
+        if do_send:
+            self.gather_and_reply(request_responses)
+
+    def gather_and_reply(self, request_responses: dict):
+        """
+        Gather request responses from all plugins and send them to the
+        CodeEditor instance that requested them.
+        """
+        req_type = request_responses['req_type']
+        req_id_responses = request_responses['sources']
+        response_instance = request_responses['response_instance']
+        logger.debug('Gather responses for {0}'.format(req_type))
+
+        if req_type == CompletionRequestTypes.DOCUMENT_COMPLETION:
+            responses = self.gather_completions(req_id_responses)
+        else:
+            responses = self.gather_responses(req_type, req_id_responses)
+
+        try:
+            response_instance.handle_response(req_type, responses)
+        except RuntimeError:
+            # This is triggered when a codeeditor instance has been
+            # removed before the response can be processed.
+            pass
+
+    def gather_completions(self, req_id_responses: dict):
+        """Gather completion responses from plugins."""
path: spyder/plugins/completion/plugin.py
text: @user1:
```suggestion
        """Gather completion responses from providers."""
```

diff_hunk: @@ -327,10 +327,10 @@ def report_open_file(self, options):
                 codeeditor.register_completion_capabilities(
                     self.completion_capabilities[language.lower()])
                 codeeditor.start_completion_services()
-            elif self.main.completions.is_fallback_only(language.lower()):
-                # This is required to use fallback completions for files
-                # without a language server.
-                codeeditor.start_completion_services()
+            # elif self.main.completions.is_fallback_only(language.lower()):
+            # This is required to use fallback completions for files
+            # without a language server.
+            codeeditor.start_completion_services()
path: spyder/plugins/editor/plugin.py
text: @user1:
I think this change is incorrect. We can't start the completion services unconditionally or things will break.

I mean, it took me a long time to fix this so I'd like to hear a good reason on why your changed this.

diff_hunk: @@ -5,54 +5,57 @@
 # (see spyder/__init__.py for details)
 
 """
-Fallback completion plugin.
+Fallback completion provider.
 
-Wraps FallbackActor to provide compatibility with SpyderCompletionPlugin API.
+Wraps FallbackActor to provide compatibility with SpyderCompletionProvider API.
 """
 
 # Standard library imports
 import logging
 
 # Local imports
-from spyder.plugins.completion.manager.api import SpyderCompletionPlugin
-from spyder.plugins.completion.fallback.actor import FallbackActor
+from spyder.config.base import _
+from spyder.plugins.completion.api import SpyderCompletionProvider
+from spyder.plugins.completion.providers.fallback.actor import FallbackActor
 
 
 logger = logging.getLogger(__name__)
 
 
-class FallbackPlugin(SpyderCompletionPlugin):
-    CONF_SECTION = 'fallback-completions'
-    CONF_FILE = False
+class FallbackProvider(SpyderCompletionProvider):
     COMPLETION_CLIENT_NAME = 'fallback'
+    DEFAULT_ORDER = 3
 
-    def __init__(self, parent):
-        SpyderCompletionPlugin.__init__(self, parent)
+    def __init__(self, parent, config):
+        SpyderCompletionProvider.__init__(self, parent, config)
         self.fallback_actor = FallbackActor(self)
         self.fallback_actor.sig_fallback_ready.connect(
-            lambda: self.sig_plugin_ready.emit(self.COMPLETION_CLIENT_NAME))
+            lambda: self.sig_provider_ready.emit(self.COMPLETION_CLIENT_NAME))
         self.fallback_actor.sig_set_tokens.connect(
             lambda _id, resp: self.sig_response_ready.emit(
                 self.COMPLETION_CLIENT_NAME, _id, resp))
         self.started = False
         self.requests = {}
-        self.update_configuration()
 
-    def start_client(self, language):
+    def get_name(self):
+        return _('Fallback')
+
+    def start_provider(self, language):
         return self.started
 
     def start(self):
-        if not self.started and self.enabled:
+        if not self.started:
             self.fallback_actor.start()
             self.started = True
 
     def shutdown(self):
         if self.started:
             self.fallback_actor.stop()
+            self.started = False
 
     def send_request(self, language, req_type, req, req_id=None):
-        if not self.enabled:
-            return
+        # if not self.enabled:
+        #     return
path: spyder/plugins/completion/providers/fallback/provider.py
text: @user1:
Please remove this if it's unnecessary.

diff_hunk: @@ -621,7 +621,7 @@ def __init__(self, parent, actions):
         self.analysis_timer.timeout.connect(self.analyze_script)
 
         # Update filename label
-        self.editor_focus_changed.connect(self.update_fname_label)
+        self.editor_focus_changed.connect(lambda x, y: self.update_fname_label)
path: spyder/plugins/editor/widgets/editor.py
text: @user1:
```suggestion
        self.editor_focus_changed.connect(lambda x, y: self.update_fname_label())
```

I think this is required to call the corresponding method.

diff_hunk: @@ -37,6 +37,11 @@
 from spyder.widgets.comboboxes import FileComboBox
 
 
+class BaseConfigTab(QWidget):
+    """Stub class to declare a config page."""
path: spyder/plugins/preferences/api.py
text: @user1:
```suggestion
    """Stub class to declare a config tab."""
```

diff_hunk: @@ -82,7 +82,8 @@ def register(self):
 
         # Connect to Editor
         widget.sig_edit_goto_requested.connect(editor.load)
-        editor.sig_editor_focus_changed.connect(self._set_filename)
+        editor.sig_editor_focus_changed.connect(
+            lambda x, y: self._set_filename)
path: spyder/plugins/pylint/plugin.py
text: @user1:
```suggestion
            lambda x, y: self._set_filename())
```

I think this is also required (not really sure).