Ejemplo n.º 1
0
def test_load_plugins():
    from pathlib import Path

    pm = PluginManager()
    p = Path(__file__).parent / "_resources" / "plugins"
    assert p.exists()
    assert pm.load_plugins_from(p) == 1
    for impl in pm.get_implementations(EPFoo):
        assert impl.Foo() == "from_plugin"
Ejemplo n.º 2
0
def test_plugins():

    from robocorp_ls_core.pluginmanager import NotRegisteredError

    pm = PluginManager()
    pm.register(EPFoo, FooImpl, keep_instance=True)

    with pytest.raises(InstanceAlreadyRegisteredError):
        pm.register(EPFoo, AnotherFooImpl, keep_instance=True)

    foo = pm.get_instance(EPFoo)
    assert pm.get_instance(EPFoo) is foo

    assert pm[EPFoo] is foo
    assert pm["EPFoo"] is foo

    # It's only registered in a way where the instance is kept
    assert not pm.get_implementations(EPFoo)

    assert not pm.get_implementations(EPBar)
    with pytest.raises(NotRegisteredError):
        pm.get_instance(EPBar)

    pm.register(EPFoo, AnotherFooImpl, context="context2", keep_instance=True)

    assert len(list(pm.iter_existing_instances(EPFoo))) == 1
    assert isinstance(pm.get_instance(EPFoo, context="context2"),
                      AnotherFooImpl)

    assert len(list(pm.iter_existing_instances(EPFoo))) == 2
    assert set(pm.iter_existing_instances(EPFoo)) == set(
        [pm.get_instance(EPFoo, context="context2"),
         pm.get_instance(EPFoo)])

    # Request using a string.
    assert len(list(pm.iter_existing_instances("EPFoo"))) == 2
    assert set(pm.iter_existing_instances("EPFoo")) == set(
        [pm.get_instance(EPFoo, context="context2"),
         pm.get_instance("EPFoo")])
class RobocorpLanguageServer(PythonLanguageServer):

    # V2: save the account info along to validate user.
    CLOUD_LIST_WORKSPACE_CACHE_KEY = "CLOUD_LIST_WORKSPACE_CACHE_V2"
    PACKAGE_ACCESS_LRU_CACHE_KEY = "PACKAGE_ACCESS_LRU_CACHE"

    def __init__(self, read_stream, write_stream):
        from robocorp_code.rcc import Rcc
        from robocorp_ls_core.cache import DirCache
        from robocorp_ls_core.pluginmanager import PluginManager
        from robocorp_ls_core.ep_providers import DefaultConfigurationProvider
        from robocorp_ls_core.ep_providers import EPConfigurationProvider
        from robocorp_ls_core.ep_providers import DefaultDirCacheProvider
        from robocorp_ls_core.ep_providers import EPDirCacheProvider
        from robocorp_ls_core.ep_providers import DefaultEndPointProvider
        from robocorp_ls_core.ep_providers import EPEndPointProvider

        user_home = os.getenv("ROBOCORP_CODE_USER_HOME", None)
        if user_home is None:
            user_home = os.path.expanduser("~")
        cache_dir = os.path.join(user_home, ".robocorp-code", ".cache")

        log.debug(f"Cache dir: {cache_dir}")

        self._dir_cache = DirCache(cache_dir)
        self._rcc = Rcc(self)
        self._track = True
        self._local_list_robots_cache: Dict[
            Path, CachedFileInfo[LocalRobotMetadataInfoDict]
        ] = {}
        PythonLanguageServer.__init__(self, read_stream, write_stream)

        self._pm = PluginManager()
        self._config_provider = DefaultConfigurationProvider(self.config)
        self._pm.set_instance(EPConfigurationProvider, self._config_provider)
        self._pm.set_instance(
            EPDirCacheProvider, DefaultDirCacheProvider(self._dir_cache)
        )
        self._pm.set_instance(
            EPEndPointProvider, DefaultEndPointProvider(self._endpoint)
        )
        from robocorp_code.plugins.resolve_interpreter import register_plugins

        register_plugins(self._pm)

    @overrides(PythonLanguageServer.m_initialize)
    def m_initialize(
        self,
        processId=None,
        rootUri=None,
        rootPath=None,
        initializationOptions=None,
        workspaceFolders=None,
        **_kwargs,
    ) -> dict:
        ret = PythonLanguageServer.m_initialize(
            self,
            processId=processId,
            rootUri=rootUri,
            rootPath=rootPath,
            initializationOptions=initializationOptions,
            workspaceFolders=workspaceFolders,
        )

        if initializationOptions and isinstance(initializationOptions, dict):
            self._track = not initializationOptions.get("do-not-track", False)

        from robocorp_code import __version__

        self._feedback_metric("vscode.started", __version__)
        return ret

    def _feedback_metric(self, name, value="+1"):
        if not self._track:
            return

        from robocorp_ls_core.timeouts import TimeoutTracker

        timeout_tracker = TimeoutTracker.get_singleton()
        timeout_tracker.call_on_timeout(
            0.1, partial(self._rcc.feedack_metric, name, value)
        )

    @overrides(PythonLanguageServer.cancel_lint)
    def cancel_lint(self, doc_uri):
        pass  # no-op

    @overrides(PythonLanguageServer.lint)
    def lint(self, doc_uri, is_saved):
        pass  # no-op

    @overrides(PythonLanguageServer._create_config)
    def _create_config(self) -> IConfig:
        from robocorp_code.robocorp_config import RobocorpConfig

        return RobocorpConfig()

    @overrides(PythonLanguageServer.capabilities)
    def capabilities(self):
        from robocorp_ls_core.lsp import TextDocumentSyncKind
        from robocorp_code.commands import ALL_SERVER_COMMANDS

        server_capabilities = {
            "codeActionProvider": False,
            # "codeLensProvider": {
            #     "resolveProvider": False,  # We may need to make this configurable
            # },
            "completionProvider": {
                "resolveProvider": False  # We know everything ahead of time
            },
            "documentFormattingProvider": False,
            "documentHighlightProvider": False,
            "documentRangeFormattingProvider": False,
            "documentSymbolProvider": False,
            "definitionProvider": True,
            "executeCommandProvider": {"commands": ALL_SERVER_COMMANDS},
            "hoverProvider": False,
            "referencesProvider": False,
            "renameProvider": False,
            "foldingRangeProvider": False,
            "textDocumentSync": {
                "change": TextDocumentSyncKind.INCREMENTAL,
                "save": {"includeText": False},
                "openClose": True,
            },
            "workspace": {
                "workspaceFolders": {"supported": True, "changeNotifications": True}
            },
        }
        log.info("Server capabilities: %s", server_capabilities)
        return server_capabilities

    def m_workspace__execute_command(self, command=None, arguments=()) -> Any:
        return command_dispatcher.dispatch(self, command, arguments)

    @command_dispatcher(commands.ROBOCORP_IS_LOGIN_NEEDED_INTERNAL)
    def _is_login_needed_internal(self) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        with progress_context(
            self._endpoint, "Validating cloud credentials", self._dir_cache
        ):
            login_needed = not self._rcc.credentials_valid()
        return {"success": login_needed, "message": None, "result": login_needed}

    @command_dispatcher(commands.ROBOCORP_CLOUD_LOGIN_INTERNAL)
    def _cloud_login(self, params: CloudLoginParamsDict) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.login")

        # When new credentials are added we need to remove existing caches.
        self._dir_cache.discard(self.CLOUD_LIST_WORKSPACE_CACHE_KEY)

        credentials = params["credentials"]
        with progress_context(
            self._endpoint, "Adding cloud credentials", self._dir_cache
        ):
            result = self._rcc.add_credentials(credentials)
            if not result.success:
                return result.as_dict()

            result = self._rcc.credentials_valid()
        return {"success": result, "message": None, "result": result}

    @command_dispatcher(commands.ROBOCORP_SAVE_IN_DISK_LRU)
    def _save_in_disk_lru(self, params: dict) -> ActionResultDict:
        name = params["name"]
        entry = params["entry"]
        lru_size = params["lru_size"]
        try:
            cache_lru_list = self._dir_cache.load(name, list)
        except:
            cache_lru_list = []

        try:
            if cache_lru_list[0] == entry:
                # Nothing to do as it already matches.
                return {"success": True, "message": "", "result": entry}

            cache_lru_list.remove(entry)
        except:
            pass  # If empty or if entry is not there, just proceed.

        if len(cache_lru_list) >= lru_size:
            cache_lru_list = cache_lru_list[:-1]

        cache_lru_list.insert(0, entry)
        self._dir_cache.store(name, cache_lru_list)
        return {"success": True, "message": "", "result": entry}

    @command_dispatcher(commands.ROBOCORP_LOAD_FROM_DISK_LRU, list)
    def _load_from_disk_lru(self, params: dict) -> ActionResultDict:
        try:
            name = params["name"]
            cache_lru_list = self._dir_cache.load(name, list)
        except:
            cache_lru_list = []

        return cache_lru_list

    def _get_sort_key_info(self):
        try:
            cache_lru_list: List[PackageInfoInLRUDict] = self._dir_cache.load(
                self.PACKAGE_ACCESS_LRU_CACHE_KEY, list
            )
        except KeyError:
            cache_lru_list = []
        DEFAULT_SORT_KEY = 10
        ws_id_and_pack_id_to_lru_index: Dict = {}
        for i, entry in enumerate(cache_lru_list):

            if i >= DEFAULT_SORT_KEY:
                break

            if isinstance(entry, dict):
                ws_id = entry.get("workspace_id")
                pack_id = entry.get("package_id")
                if ws_id is not None and pack_id is not None:
                    key = (ws_id, pack_id)
                    ws_id_and_pack_id_to_lru_index[key] = i
        return ws_id_and_pack_id_to_lru_index

    @command_dispatcher(commands.ROBOCORP_CLOUD_LIST_WORKSPACES_INTERNAL)
    def _cloud_list_workspaces(
        self, params: CloudListWorkspaceDict
    ) -> ListWorkspacesActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        DEFAULT_SORT_KEY = 10
        package_info: PackageInfoDict
        ws_dict: WorkspaceInfoDict

        ws_id_and_pack_id_to_lru_index = self._get_sort_key_info()
        curr_account_info = self._rcc.last_verified_account_info
        if curr_account_info is None:
            curr_account_info = self._rcc.get_valid_account_info()
            if curr_account_info is None:
                return {
                    "success": False,
                    "message": "Unable to get workspace info (no user is logged in).",
                    "result": None,
                }

        account_cache_key = (curr_account_info.account, curr_account_info.identifier)

        if not params.get("refresh", True):
            try:
                cached: ListWorkspaceCachedInfoDict = self._dir_cache.load(
                    self.CLOUD_LIST_WORKSPACE_CACHE_KEY, dict
                )
            except KeyError:
                pass
            else:
                # We need to update the sort key when it's gotten from the cache.
                try:
                    if account_cache_key == tuple(cached.get("account_cache_key", ())):
                        for ws_dict in cached["ws_info"]:
                            for package_info in ws_dict["packages"]:
                                key = (package_info["workspaceId"], package_info["id"])
                                sort_key = "%05d%s" % (
                                    ws_id_and_pack_id_to_lru_index.get(
                                        key, DEFAULT_SORT_KEY
                                    ),
                                    package_info["name"].lower(),
                                )

                                package_info["sortKey"] = sort_key
                        return {
                            "success": True,
                            "message": None,
                            "result": cached["ws_info"],
                        }
                except Exception:
                    log.exception(
                        "Error computing new sort keys for cached entry. Refreshing and proceeding."
                    )

        last_error_result = None

        with progress_context(
            self._endpoint, "Listing cloud workspaces", self._dir_cache
        ):
            ws: IRccWorkspace
            ret: List[WorkspaceInfoDict] = []
            result = self._rcc.cloud_list_workspaces()
            if not result.success:
                return result.as_dict()

            workspaces = result.result
            for ws in workspaces:
                packages: List[PackageInfoDict] = []

                activity_package: IRccRobotMetadata
                activities_result = self._rcc.cloud_list_workspace_robots(
                    ws.workspace_id
                )
                if not activities_result.success:
                    # If we can't list the robots of a specific workspace, just skip it
                    # (the log should still show it but we can proceed to list the
                    # contents of other workspaces).
                    last_error_result = activities_result
                    continue

                workspace_activities = activities_result.result
                for activity_package in workspace_activities:

                    key = (ws.workspace_id, activity_package.robot_id)
                    sort_key = "%05d%s" % (
                        ws_id_and_pack_id_to_lru_index.get(key, DEFAULT_SORT_KEY),
                        activity_package.robot_name.lower(),
                    )

                    package_info = {
                        "name": activity_package.robot_name,
                        "id": activity_package.robot_id,
                        "sortKey": sort_key,
                        "workspaceId": ws.workspace_id,
                        "workspaceName": ws.workspace_name,
                    }
                    packages.append(package_info)

                ws_dict = {
                    "workspaceName": ws.workspace_name,
                    "workspaceId": ws.workspace_id,
                    "packages": packages,
                }
                ret.append(ws_dict)

        if not ret and last_error_result is not None:
            return last_error_result.as_dict()

        if ret:  # Only store if we got something.
            store: ListWorkspaceCachedInfoDict = {
                "ws_info": ret,
                "account_cache_key": account_cache_key,
            }
            self._dir_cache.store(self.CLOUD_LIST_WORKSPACE_CACHE_KEY, store)
        return {"success": True, "message": None, "result": ret}

    @command_dispatcher(commands.ROBOCORP_CREATE_ROBOT_INTERNAL)
    def _create_robot(self, params: CreateRobotParamsDict) -> ActionResultDict:
        self._feedback_metric("vscode.create.robot")
        directory = params["directory"]
        template = params["template"]
        name = params["name"]

        return self._rcc.create_robot(template, os.path.join(directory, name)).as_dict()

    @command_dispatcher(commands.ROBOCORP_LIST_ROBOT_TEMPLATES_INTERNAL)
    def _list_activity_templates(self, params=None) -> ActionResultDict:
        result = self._rcc.get_template_names()
        return result.as_dict()

    def _get_robot_metadata(
        self,
        sub: Path,
        curr_cache: Dict[Path, CachedFileInfo[LocalRobotMetadataInfoDict]],
        new_cache: Dict[Path, CachedFileInfo[LocalRobotMetadataInfoDict]],
    ) -> Optional[LocalRobotMetadataInfoDict]:
        """
        Note that we get the value from the current cache and then put it in
        the new cache if it's still valid (that way we don't have to mutate
        the old cache to remove stale values... all that's valid is put in
        the new cache).
        """
        robot_yaml = sub / "robot.yaml"

        cached_file_info: Optional[
            CachedFileInfo[LocalRobotMetadataInfoDict]
        ] = curr_cache.get(sub)
        if cached_file_info is not None:
            if cached_file_info.is_cache_valid():
                new_cache[sub] = cached_file_info
                return cached_file_info.value

        if robot_yaml.exists():
            from robocorp_ls_core import yaml_wrapper

            try:

                def get_robot_metadata(robot_yaml: Path):
                    name = robot_yaml.parent.name
                    with robot_yaml.open("r", encoding="utf-8") as stream:
                        yaml_contents = yaml_wrapper.load(stream)
                        name = yaml_contents.get("name", name)

                    robot_metadata: LocalRobotMetadataInfoDict = {
                        "directory": str(sub),
                        "filePath": str(robot_yaml),
                        "name": name,
                        "yamlContents": yaml_contents,
                    }
                    return robot_metadata

                cached_file_info = new_cache[sub] = CachedFileInfo(
                    robot_yaml, get_robot_metadata
                )
                return cached_file_info.value

            except:
                log.exception(f"Unable to get load robot metadata for: {robot_yaml}")

        return None

    @command_dispatcher(commands.ROBOCORP_RUN_IN_RCC_INTERNAL)
    def _run_in_rcc_internal(self, params=RunInRccParamsDict) -> ActionResultDict:
        try:
            args = params["args"]
            ret = self._rcc._run_rcc(args)
        except Exception as e:
            log.exception(f"Error running in RCC: {params}.")
            return dict(success=False, message=str(e), result=None)
        return ret.as_dict()

    @command_dispatcher(commands.ROBOCORP_LOCAL_LIST_ROBOTS_INTERNAL)
    def _local_list_robots(self, params=None) -> ActionResultDictLocalRobotMetadata:
        curr_cache = self._local_list_robots_cache
        new_cache: Dict[Path, CachedFileInfo[LocalRobotMetadataInfoDict]] = {}

        ret: List[LocalRobotMetadataInfoDict] = []
        try:
            ws = self.workspace
            if ws:
                for folder_path in ws.get_folder_paths():
                    # Check the root directory itself for the robot.yaml.
                    p = Path(folder_path)
                    robot_metadata = self._get_robot_metadata(p, curr_cache, new_cache)
                    if robot_metadata is not None:
                        ret.append(robot_metadata)
                    elif p.is_dir():
                        for sub in p.iterdir():
                            robot_metadata = self._get_robot_metadata(
                                sub, curr_cache, new_cache
                            )
                            if robot_metadata is not None:
                                ret.append(robot_metadata)

            ret.sort(key=lambda dct: dct["name"])
        except Exception as e:
            log.exception("Error listing robots.")
            return dict(success=False, message=str(e), result=None)
        finally:
            # Set the new cache after we finished computing all entries.
            self._local_list_robots_cache = new_cache

        return dict(success=True, message=None, result=ret)

    def _validate_directory(self, directory) -> Optional[str]:
        if not os.path.exists(directory):
            return f"Expected: {directory} to exist."

        if not os.path.isdir(directory):
            return f"Expected: {directory} to be a directory."
        return None

    def _add_package_info_to_access_lru(self, workspace_id, package_id, directory):
        import time

        try:
            lst: List[PackageInfoInLRUDict] = self._dir_cache.load(
                self.PACKAGE_ACCESS_LRU_CACHE_KEY, list
            )
        except KeyError:
            lst = []

        new_lst: List[PackageInfoInLRUDict] = [
            {
                "workspace_id": workspace_id,
                "package_id": package_id,
                "directory": directory,
                "time": time.time(),
            }
        ]
        for i, entry in enumerate(lst):
            if isinstance(entry, dict):
                if (
                    entry.get("package_id") == package_id
                    and entry.get("workspace_id") == workspace_id
                ):
                    continue  # Skip this one (we moved it to the start of the LRU).
                new_lst.append(entry)

            if i == 5:
                break  # Max number of items in the LRU reached.

        self._dir_cache.store(self.PACKAGE_ACCESS_LRU_CACHE_KEY, new_lst)

    @command_dispatcher(commands.ROBOCORP_UPLOAD_TO_EXISTING_ROBOT_INTERNAL)
    def _upload_to_existing_activity(
        self, params: UploadRobotParamsDict
    ) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.upload.existing")

        directory = params["directory"]
        error_msg = self._validate_directory(directory)
        if error_msg:
            return {"success": False, "message": error_msg, "result": None}

        workspace_id = params["workspaceId"]
        robot_id = params["robotId"]
        with progress_context(
            self._endpoint, "Uploading to existing robot", self._dir_cache
        ):

            result = self._rcc.cloud_set_robot_contents(
                directory, workspace_id, robot_id
            )
            self._add_package_info_to_access_lru(workspace_id, robot_id, directory)

        return result.as_dict()

    @command_dispatcher(commands.ROBOCORP_UPLOAD_TO_NEW_ROBOT_INTERNAL)
    def _upload_to_new_robot(
        self, params: UploadNewRobotParamsDict
    ) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.upload.new")

        directory = params["directory"]
        error_msg = self._validate_directory(directory)
        if error_msg:
            return {"success": False, "message": error_msg, "result": None}

        workspace_id = params["workspaceId"]
        robot_name = params["robotName"]

        # When we upload to a new activity, clear the existing cache key.
        self._dir_cache.discard(self.CLOUD_LIST_WORKSPACE_CACHE_KEY)
        with progress_context(
            self._endpoint, "Uploading to new robot", self._dir_cache
        ):
            new_robot_result = self._rcc.cloud_create_robot(workspace_id, robot_name)
            if not new_robot_result.success:
                return new_robot_result.as_dict()

            robot_id = new_robot_result.result
            if not robot_id:
                return dict(
                    success=False,
                    message="Expected to have package id from creating new activity.",
                    result=None,
                )

            result = self._rcc.cloud_set_robot_contents(
                directory, workspace_id, robot_id
            )
            self._add_package_info_to_access_lru(workspace_id, robot_id, directory)
        return result.as_dict()

    @command_dispatcher(commands.ROBOCORP_GET_PLUGINS_DIR, str)
    def _get_plugins_dir(self, params=None) -> str:
        return str(Path(__file__).parent / "plugins")

    @command_dispatcher(
        commands.ROBOCORP_COMPUTE_ROBOT_LAUNCH_FROM_ROBOCORP_CODE_LAUNCH
    )
    def _compute_robot_launch_from_robocorp_code_launch(
        self, params: dict
    ) -> ActionResultDictRobotLaunch:
        from robocorp_code import compute_launch

        name: Optional[str] = params.get("name")
        request: Optional[str] = params.get("request")
        task: Optional[str] = params.get("task")
        robot: Optional[str] = params.get("robot")
        additional_pythonpath_entries: Optional[List[str]] = params.get(
            "additionalPythonpathEntries"
        )
        env: Optional[Dict[str, str]] = params.get("env")
        python_exe: Optional[str] = params.get("pythonExe")

        return compute_launch.compute_robot_launch_from_robocorp_code_launch(
            name, request, task, robot, additional_pythonpath_entries, env, python_exe
        )

    @command_dispatcher(commands.ROBOCORP_RESOLVE_INTERPRETER, dict)
    def _resolve_interpreter(self, params=None) -> ActionResultDict:
        from robocorp_ls_core.ep_resolve_interpreter import EPResolveInterpreter
        from robocorp_ls_core.ep_resolve_interpreter import IInterpreterInfo

        try:
            from robocorp_ls_core import uris

            target_robot: str = params.get("target_robot")

            for ep in self._pm.get_implementations(EPResolveInterpreter):
                interpreter_info: IInterpreterInfo = ep.get_interpreter_info_for_doc_uri(
                    uris.from_fs_path(target_robot)
                )
                if interpreter_info is not None:
                    return {
                        "success": True,
                        "message": None,
                        "result": {
                            "pythonExe": interpreter_info.get_python_exe(),
                            "environ": interpreter_info.get_environ(),
                            "additionalPythonpathEntries": interpreter_info.get_additional_pythonpath_entries(),
                        },
                    }
        except Exception as e:
            log.exception(f"Error resolving interpreter. Args: {params}")
            return {"success": False, "message": str(e), "result": None}

        # i.e.: no error but we couldn't find an interpreter.
        return {"success": True, "message": "", "result": None}
class RobotFrameworkLanguageServer(PythonLanguageServer):
    def __init__(self, rx, tx) -> None:
        from robocorp_ls_core.pluginmanager import PluginManager
        from robotframework_ls.server_manager import ServerManager
        from robotframework_ls.ep_providers import DefaultConfigurationProvider
        from robotframework_ls.ep_providers import DefaultEndPointProvider
        from robotframework_ls.ep_providers import DefaultDirCacheProvider

        PythonLanguageServer.__init__(self, rx, tx)

        from robocorp_ls_core.cache import DirCache

        from robotframework_ls import robot_config

        home = robot_config.get_robotframework_ls_home()
        cache_dir = os.path.join(home, ".cache")

        log.debug(f"Cache dir: {cache_dir}")

        self._dir_cache = DirCache(cache_dir)

        self._pm = PluginManager()
        self._config_provider = DefaultConfigurationProvider(self.config)
        self._pm.set_instance(EPConfigurationProvider, self._config_provider)
        self._pm.set_instance(EPDirCacheProvider,
                              DefaultDirCacheProvider(self._dir_cache))
        self._pm.set_instance(EPEndPointProvider,
                              DefaultEndPointProvider(self._endpoint))
        self._server_manager = ServerManager(self._pm, language_server=self)
        self._lint_manager = _LintManager(self._server_manager,
                                          self._lsp_messages)

    @overrides(PythonLanguageServer._create_config)
    def _create_config(self) -> IConfig:
        from robotframework_ls.robot_config import RobotConfig

        return RobotConfig()

    @overrides(PythonLanguageServer._on_workspace_set)
    def _on_workspace_set(self, workspace: IWorkspace):
        PythonLanguageServer._on_workspace_set(self, workspace)
        self._server_manager.set_workspace(workspace)

    @overrides(PythonLanguageServer._create_workspace)
    def _create_workspace(self, root_uri, workspace_folders):
        from robotframework_ls.impl.robot_workspace import RobotWorkspace

        return RobotWorkspace(root_uri, workspace_folders, generate_ast=False)

    def m_initialize(
        self,
        processId=None,
        rootUri=None,
        rootPath=None,
        initializationOptions=None,
        workspaceFolders=None,
        **_kwargs,
    ) -> dict:
        ret = PythonLanguageServer.m_initialize(
            self,
            processId=processId,
            rootUri=rootUri,
            rootPath=rootPath,
            initializationOptions=initializationOptions,
            workspaceFolders=workspaceFolders,
            **_kwargs,
        )

        initialization_options = initializationOptions
        if initialization_options:
            plugins_dir = initialization_options.get("pluginsDir")
            if isinstance(plugins_dir, str):
                if not os.path.isdir(plugins_dir):
                    log.critical(f"Expected: {plugins_dir} to be a directory.")
                else:
                    self._pm.load_plugins_from(Path(plugins_dir))

        return ret

    @overrides(PythonLanguageServer.capabilities)
    def capabilities(self):
        from robocorp_ls_core.lsp import TextDocumentSyncKind

        server_capabilities = {
            "codeActionProvider": False,
            "codeLensProvider": {
                "resolveProvider":
                True,  # We may need to make this configurable
            },
            "completionProvider": {
                "resolveProvider": False  # We know everything ahead of time
            },
            # "semanticTokensProvider": {
            #     "legend": {
            #         "tokenTypes": ["keyword", "comment", "string", "number", "operator"],
            #         "tokenModifiers": ["declaration", "definition", "documentation",]
            #     },
            #     "range": True,
            #     "full": True
            # },
            "documentFormattingProvider": True,
            "documentHighlightProvider": True,
            "documentRangeFormattingProvider": False,
            "documentSymbolProvider": False,
            "definitionProvider": True,
            "executeCommandProvider": {
                "commands":
                ["robot.addPluginsDir", "robot.resolveInterpreter"]
            },
            "hoverProvider": True,
            "referencesProvider": False,
            "renameProvider": False,
            "foldingRangeProvider": True,
            # Note that there are no auto-trigger characters (there's no good
            # character as there's no `(` for parameters and putting it as a
            # space becomes a bit too much).
            "signatureHelpProvider": {
                "triggerCharacters": []
            },
            "textDocumentSync": {
                "change": TextDocumentSyncKind.INCREMENTAL,
                "save": {
                    "includeText": False
                },
                "openClose": True,
            },
            "workspace": {
                "workspaceFolders": {
                    "supported": True,
                    "changeNotifications": True
                }
            },
            "workspaceSymbolProvider": {
                "workDoneProgress": False
            },
        }
        log.info("Server capabilities: %s", server_capabilities)
        return server_capabilities

    def m_workspace__symbol(self, query: Optional[str] = None) -> Any:
        api_client = self._server_manager.get_workspace_symbols_api_client()
        if api_client is not None:
            ret = partial(self._threaded_workspace_symbol, api_client, query)
            ret = require_monitor(ret)
            return ret

        log.info("Unable to search workspace symbols (no api available).")
        return None  # Unable to get the api.

    def _threaded_workspace_symbol(
        self,
        api_client: IRobotFrameworkApiClient,
        query: Optional[str],
        monitor: IMonitor,
    ):
        from robocorp_ls_core.client_base import wait_for_message_matcher

        # Asynchronous completion.
        message_matcher: Optional[
            IIdMessageMatcher] = api_client.request_workspace_symbols(query)
        if message_matcher is None:
            log.debug("Message matcher for workspace symbols returned None.")
            return None

        if wait_for_message_matcher(
                message_matcher,
                api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    def m_workspace__execute_command(self, command=None, arguments=()) -> Any:
        if command == "robot.addPluginsDir":
            directory: str = arguments[0]
            assert os.path.isdir(
                directory), f"Expected: {directory} to be a directory."
            self._pm.load_plugins_from(Path(directory))
            return True

        elif command == "robot.resolveInterpreter":
            try:
                from robocorp_ls_core import uris
                from robotframework_ls.ep_resolve_interpreter import (
                    EPResolveInterpreter, )
                from robotframework_ls.ep_resolve_interpreter import IInterpreterInfo

                target_robot: str = arguments[0]

                for ep in self._pm.get_implementations(EPResolveInterpreter):
                    interpreter_info: IInterpreterInfo = ep.get_interpreter_info_for_doc_uri(
                        uris.from_fs_path(target_robot))
                    if interpreter_info is not None:
                        return {
                            "pythonExe":
                            interpreter_info.get_python_exe(),
                            "environ":
                            interpreter_info.get_environ(),
                            "additionalPythonpathEntries":
                            interpreter_info.get_additional_pythonpath_entries(
                            ),
                        }
            except:
                log.exception(
                    f"Error resolving interpreter. Args: {arguments}")

    @overrides(PythonLanguageServer.m_workspace__did_change_configuration)
    @log_and_silence_errors(log)
    def m_workspace__did_change_configuration(self, **kwargs):
        PythonLanguageServer.m_workspace__did_change_configuration(
            self, **kwargs)
        self._server_manager.set_config(self.config)

    # --- Methods to forward to the api

    @overrides(PythonLanguageServer.m_shutdown)
    @log_and_silence_errors(log)
    def m_shutdown(self, **kwargs):
        self._server_manager.shutdown()

        PythonLanguageServer.m_shutdown(self, **kwargs)

    @overrides(PythonLanguageServer.m_exit)
    @log_and_silence_errors(log)
    def m_exit(self, **kwargs):
        self._server_manager.exit()

        PythonLanguageServer.m_exit(self, **kwargs)

    def m_text_document__document_highlight(self, *args,
                                            **kwargs) -> Optional[list]:
        return []

    def m_text_document__formatting(self,
                                    textDocument=None,
                                    options=None) -> Optional[list]:
        source_format_rf_api_client = (
            self._server_manager.get_source_format_rf_api_client())
        if source_format_rf_api_client is None:
            log.info("Unable to get API for source format.")
            return []

        message_matcher = source_format_rf_api_client.request_source_format(
            text_document=textDocument, options=options)
        if message_matcher is None:
            raise RuntimeError(
                "Error requesting code formatting (message_matcher==None).")
        curtime = time.time()
        maxtime = curtime + DEFAULT_COMPLETIONS_TIMEOUT

        # i.e.: wait X seconds for the code format and bail out if we
        # can't get it.
        available_time = maxtime - time.time()
        if available_time <= 0:
            raise RuntimeError(
                "Code formatting timed-out (available_time <= 0).")

        if message_matcher.event.wait(available_time):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result
                else:
                    return []
        raise RuntimeError("Code formatting timed-out.")

    @overrides(PythonLanguageServer.m_text_document__did_close)
    def m_text_document__did_close(self, textDocument=None, **_kwargs):
        self._server_manager.forward(("api", "lint"), "textDocument/didClose",
                                     {"textDocument": textDocument})
        PythonLanguageServer.m_text_document__did_close(
            self, textDocument=textDocument, **_kwargs)

    @overrides(PythonLanguageServer.m_text_document__did_open)
    def m_text_document__did_open(self, textDocument=None, **_kwargs):
        self._server_manager.forward(("api", "lint"), "textDocument/didOpen",
                                     {"textDocument": textDocument})
        PythonLanguageServer.m_text_document__did_open(
            self, textDocument=textDocument, **_kwargs)

    @overrides(PythonLanguageServer.m_text_document__did_change)
    def m_text_document__did_change(self,
                                    contentChanges=None,
                                    textDocument=None,
                                    **_kwargs):
        self._server_manager.forward(
            ("api", "lint"),
            "textDocument/didChange",
            {
                "contentChanges": contentChanges,
                "textDocument": textDocument
            },
        )
        PythonLanguageServer.m_text_document__did_change(
            self,
            contentChanges=contentChanges,
            textDocument=textDocument,
            **_kwargs)

    @overrides(PythonLanguageServer.m_workspace__did_change_workspace_folders)
    def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):
        self._server_manager.forward(
            ("api", "lint"), "workspace/didChangeWorkspaceFolders", event)
        PythonLanguageServer.m_workspace__did_change_workspace_folders(
            self, event=event, **_kwargs)

    # --- Customized implementation

    @overrides(PythonLanguageServer.lint)
    def lint(self, doc_uri, is_saved) -> None:
        self._lint_manager.schedule_lint(doc_uri, is_saved)

    @overrides(PythonLanguageServer.cancel_lint)
    def cancel_lint(self, doc_uri) -> None:
        self._lint_manager.cancel_lint(doc_uri)

    def m_text_document__definition(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            ret = partial(self._threaded_document_definition, rf_api_client,
                          doc_uri, line, col)
            ret = require_monitor(ret)
            return ret

        log.info("Unable to find definition (no api available).")
        return None  # Unable to get the api.

    @log_and_silence_errors(log)
    def _threaded_document_definition(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        line: int,
        col: int,
        monitor: IMonitor,
    ) -> Optional[list]:

        from robocorp_ls_core.client_base import wait_for_message_matchers

        workspace = self.workspace
        if not workspace:
            error_msg = "Workspace is closed."
            log.critical(error_msg)
            raise RuntimeError(error_msg)

        document = workspace.get_document(doc_uri, accept_from_file=True)
        if document is None:
            error_msg = "Unable to find document (%s) for definition." % (
                doc_uri, )
            log.critical(error_msg)
            raise RuntimeError(error_msg)

        message_matchers: List[Optional[IIdMessageMatcher]] = [
            rf_api_client.request_find_definition(doc_uri, line, col)
        ]
        accepted_message_matchers = wait_for_message_matchers(
            message_matchers,
            monitor,
            rf_api_client.request_cancel,
            DEFAULT_COMPLETIONS_TIMEOUT,
        )
        message_matcher: IMessageMatcher
        for message_matcher in accepted_message_matchers:
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    def m_text_document__completion(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(self._threaded_document_completion, rf_api_client,
                           doc_uri, line, col)
            func = require_monitor(func)
            return func

        log.info("Unable to get completions (no api available).")
        return []

    @log_and_silence_errors(log, return_on_error=[])
    def _threaded_document_completion(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        line: int,
        col: int,
        monitor: IMonitor,
    ) -> list:
        from robotframework_ls.impl.completion_context import CompletionContext
        from robotframework_ls.impl import section_completions
        from robotframework_ls.impl import snippets_completions
        from robocorp_ls_core.client_base import wait_for_message_matchers

        ws = self.workspace
        if not ws:
            log.critical("Workspace must be set before returning completions.")
            return []

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for completions." %
                         (doc_uri, ))
            return []

        ctx = CompletionContext(document, line, col, config=self.config)
        completions = []

        # Asynchronous completion.
        message_matchers: List[Optional[IIdMessageMatcher]] = []
        message_matchers.append(
            rf_api_client.request_complete_all(doc_uri, line, col))

        # These run locally (no need to get from the server).
        completions.extend(section_completions.complete(ctx))
        completions.extend(snippets_completions.complete(ctx))

        accepted_message_matchers = wait_for_message_matchers(
            message_matchers,
            monitor,
            rf_api_client.request_cancel,
            DEFAULT_COMPLETIONS_TIMEOUT,
        )
        for message_matcher in accepted_message_matchers:
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    completions.extend(result)

        return completions

    def m_text_document__signature_help(self, **kwargs):
        """
        "params": {
            "textDocument": {
                "uri": "file:///x%3A/vscode-robot/local_test/Basic/resources/keywords.robot"
            },
            "position": {"line": 7, "character": 22},
            "context": {
                "isRetrigger": False,
                "triggerCharacter": " ",
                "triggerKind": 2,
            },
        },
        """
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(self._signature_help, rf_api_client, doc_uri, line,
                           col)
            func = require_monitor(func)
            return func

        log.info("Unable to get signature (no api available).")
        return []

    @log_and_silence_errors(log)
    def _signature_help(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        line: int,
        col: int,
        monitor: Monitor,
    ) -> Optional[dict]:
        from robocorp_ls_core.client_base import wait_for_message_matcher

        ws = self.workspace
        if not ws:
            log.critical(
                "Workspace must be set before getting signature help.")
            return None

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for completions." %
                         (doc_uri, ))
            return None

        # Asynchronous completion.
        message_matcher: Optional[
            IIdMessageMatcher] = rf_api_client.request_signature_help(
                doc_uri, line, col)
        if message_matcher is None:
            log.debug("Message matcher for signature returned None.")
            return None

        if wait_for_message_matcher(
                message_matcher,
                rf_api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    def m_text_document__hover(self, **kwargs):
        """
        "params": {
            "textDocument": {
                "uri": "file:///x%3A/vscode-robot/local_test/Basic/resources/keywords.robot"
            },
            "position": {"line": 7, "character": 22},            
        },
        """
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(self._hover, rf_api_client, doc_uri, line, col)
            func = require_monitor(func)
            return func

        log.info("Unable to get hover (no api available).")
        return []

    @log_and_silence_errors(log)
    def _hover(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        line: int,
        col: int,
        monitor: Monitor,
    ) -> Optional[dict]:
        from robocorp_ls_core.client_base import wait_for_message_matcher

        ws = self.workspace
        if not ws:
            log.critical("Workspace must be set before getting hover.")
            return None

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for hover." %
                         (doc_uri, ))
            return None

        # Asynchronous completion.
        message_matcher: Optional[
            IIdMessageMatcher] = rf_api_client.request_hover(
                doc_uri, line, col)
        if message_matcher is None:
            log.debug("Message matcher for hover returned None.")
            return None

        if wait_for_message_matcher(
                message_matcher,
                rf_api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    def m_text_document__folding_range(self, *args,
                                       **kwargs) -> Optional[list]:
        """
        "params": {
            "textDocument": {
                "uri": "file:///x%3A/vscode-robot/local_test/Basic/resources/keywords.robot"
            }
        },
        """

        doc_uri = kwargs["textDocument"]["uri"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(self._folding_range, rf_api_client, doc_uri)
            func = require_monitor(func)
            return func

        log.info("Unable to get folding range (no api available).")
        return []

    @log_and_silence_errors(log)
    def _folding_range(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        monitor: Monitor,
    ) -> Optional[dict]:
        from robocorp_ls_core.client_base import wait_for_message_matcher

        ws = self.workspace
        if not ws:
            log.critical("Workspace must be set before getting hover.")
            return None

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for folding range." %
                         (doc_uri, ))
            return None

        # Asynchronous completion.
        message_matcher: Optional[
            IIdMessageMatcher] = rf_api_client.request_folding_range(doc_uri)
        if message_matcher is None:
            log.debug("Message matcher for folding range returned None.")
            return None

        if wait_for_message_matcher(
                message_matcher,
                rf_api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    def m_text_document__code_lens(self, *args, **kwargs) -> Optional[list]:
        """
        "params": {
            "textDocument": {
                "uri": "file:///x%3A/vscode-robot/local_test/Basic/resources/keywords.robot"
            }
        },
        """

        doc_uri = kwargs["textDocument"]["uri"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(self._code_lens, rf_api_client, doc_uri)
            func = require_monitor(func)
            return func

        log.info("Unable to get folding range (no api available).")
        return []

    @log_and_silence_errors(log)
    def _code_lens(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        monitor: Monitor,
    ) -> Optional[dict]:
        from robocorp_ls_core.client_base import wait_for_message_matcher

        ws = self.workspace
        if not ws:
            log.critical("Workspace must be set before getting hover.")
            return None

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for code lens." %
                         (doc_uri, ))
            return None

        # Asynchronous completion.
        message_matcher: Optional[
            IIdMessageMatcher] = rf_api_client.request_code_lens(doc_uri)
        if message_matcher is None:
            log.debug("Message matcher for code lens returned None.")
            return None

        if wait_for_message_matcher(
                message_matcher,
                rf_api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None
class RobotFrameworkLanguageServer(PythonLanguageServer):
    def __init__(self, rx, tx) -> None:
        from robocorp_ls_core.pluginmanager import PluginManager
        from robotframework_ls.rf_interactive_integration import _RfInterpretersManager
        from robotframework_ls.server_manager import ServerManager
        from robotframework_ls.ep_providers import DefaultConfigurationProvider
        from robotframework_ls.ep_providers import DefaultEndPointProvider
        from robotframework_ls.ep_providers import DefaultDirCacheProvider
        from robocorp_ls_core import watchdog_wrapper
        from robocorp_ls_core.remote_fs_observer_impl import RemoteFSObserver
        from robocorp_ls_core.options import Setup

        PythonLanguageServer.__init__(self, rx, tx)

        from robocorp_ls_core.cache import DirCache

        from robotframework_ls import robot_config

        home = robot_config.get_robotframework_ls_home()
        cache_dir = os.path.join(home, ".cache")

        log.debug(f"Cache dir: {cache_dir}")

        self._dir_cache = DirCache(cache_dir)

        self._pm = PluginManager()
        self._config_provider = DefaultConfigurationProvider(self.config)
        self._pm.set_instance(EPConfigurationProvider, self._config_provider)
        self._pm.set_instance(EPDirCacheProvider,
                              DefaultDirCacheProvider(self._dir_cache))
        self._pm.set_instance(EPEndPointProvider,
                              DefaultEndPointProvider(self._endpoint))
        self._rf_interpreters_manager = _RfInterpretersManager(
            self._endpoint, self._pm)

        watch_impl = os.environ.get("ROBOTFRAMEWORK_LS_WATCH_IMPL", "auto")
        if watch_impl not in ("watchdog", "fsnotify", "auto"):
            log.info(
                f"ROBOTFRAMEWORK_LS_WATCH_IMPL should be 'auto', 'watchdog' or 'fsnotify'. Found: {watch_impl} (falling back to auto)"
            )
            watch_impl = "auto"

        if watch_impl == "auto":
            # In auto mode we use watchdog for windows and fsnotify (polling)
            # for Linux and Mac. The reason for that is that on Linux and Mac
            # if big folders are watched the system may complain due to the
            # lack of resources, which may prevent the extension from working
            # properly.
            #
            # If users want to opt-in, they can change to watchdog (and
            # ideally install it to their env to get native extensions).
            if sys.platform == "win32":
                watch_impl = "watchdog"
            else:
                watch_impl = "fsnotify"

        self._fs_observer = watchdog_wrapper.create_remote_observer(
            watch_impl, (".py", ".libspec", "robot", ".resource"))
        remote_observer = typing.cast(RemoteFSObserver, self._fs_observer)
        log_file = Setup.options.log_file
        if not isinstance(log_file, str):
            log_file = None
        remote_observer.start_server(log_file=log_file)

        self._server_manager = ServerManager(self._pm, language_server=self)
        self._lint_manager = _LintManager(self._server_manager,
                                          self._lsp_messages)

    def get_remote_fs_observer_port(self) -> Optional[int]:
        from robocorp_ls_core.remote_fs_observer_impl import RemoteFSObserver

        remote_observer = typing.cast(RemoteFSObserver, self._fs_observer)
        return remote_observer.port

    @overrides(PythonLanguageServer._create_config)
    def _create_config(self) -> IConfig:
        from robotframework_ls.robot_config import RobotConfig

        return RobotConfig()

    @overrides(PythonLanguageServer._on_workspace_set)
    def _on_workspace_set(self, workspace: IWorkspace):
        PythonLanguageServer._on_workspace_set(self, workspace)
        self._server_manager.set_workspace(workspace)

    @overrides(PythonLanguageServer._obtain_fs_observer)
    def _obtain_fs_observer(self) -> IFSObserver:
        return self._fs_observer

    @overrides(PythonLanguageServer._create_workspace)
    def _create_workspace(self, root_uri: str, fs_observer: IFSObserver,
                          workspace_folders):
        from robotframework_ls.impl.robot_workspace import RobotWorkspace

        return RobotWorkspace(root_uri,
                              fs_observer,
                              workspace_folders,
                              generate_ast=False)

    def m_initialize(
        self,
        processId=None,
        rootUri=None,
        rootPath=None,
        initializationOptions=None,
        workspaceFolders=None,
        **_kwargs,
    ) -> dict:
        # capabilities = _kwargs.get("capabilities", {})
        # text_document_capabilities = capabilities.get("textDocument", {})
        # document_symbol_capabilities = text_document_capabilities.get(
        #     "documentSymbol", {}
        # )
        # hierarchical_document_symbol_support = document_symbol_capabilities.get(
        #     "hierarchicalDocumentSymbolSupport", False
        # )
        # self._hierarchical_document_symbol_support = (
        #     hierarchical_document_symbol_support
        # )

        ret = PythonLanguageServer.m_initialize(
            self,
            processId=processId,
            rootUri=rootUri,
            rootPath=rootPath,
            initializationOptions=initializationOptions,
            workspaceFolders=workspaceFolders,
            **_kwargs,
        )

        initialization_options = initializationOptions
        if initialization_options:
            plugins_dir = initialization_options.get("pluginsDir")
            if isinstance(plugins_dir, str):
                if not os.path.isdir(plugins_dir):
                    log.critical(f"Expected: {plugins_dir} to be a directory.")
                else:
                    self._pm.load_plugins_from(Path(plugins_dir))

        return ret

    @overrides(PythonLanguageServer.capabilities)
    def capabilities(self):
        from robocorp_ls_core.lsp import TextDocumentSyncKind
        from robotframework_ls.impl.semantic_tokens import TOKEN_TYPES, TOKEN_MODIFIERS
        from robotframework_ls import commands

        server_capabilities = {
            "codeActionProvider": False,
            "codeLensProvider": {
                "resolveProvider": True
            },
            "completionProvider": {
                "resolveProvider": False  # We know everything ahead of time
            },
            "documentFormattingProvider": True,
            "documentHighlightProvider": False,
            "documentRangeFormattingProvider": False,
            "documentSymbolProvider": True,
            "definitionProvider": True,
            "executeCommandProvider": {
                "commands": [
                    "robot.addPluginsDir",
                    "robot.resolveInterpreter",
                    "robot.getLanguageServerVersion",
                    "robot.getInternalInfo",
                    "robot.listTests",
                ] + commands.ALL_SERVER_COMMANDS
            },
            "hoverProvider": True,
            "referencesProvider": False,
            "renameProvider": False,
            "foldingRangeProvider": True,
            # Note that there are no auto-trigger characters (there's no good
            # character as there's no `(` for parameters and putting it as a
            # space becomes a bit too much).
            "signatureHelpProvider": {
                "triggerCharacters": []
            },
            "textDocumentSync": {
                "change": TextDocumentSyncKind.INCREMENTAL,
                "save": {
                    "includeText": False
                },
                "openClose": True,
            },
            "workspace": {
                "workspaceFolders": {
                    "supported": True,
                    "changeNotifications": True
                }
            },
            "workspaceSymbolProvider": True,
            # The one below isn't accepted by lsp4j (it's still in LSP 3.15.0).
            # "workspaceSymbolProvider": {"workDoneProgress": False},
            "semanticTokensProvider": {
                "legend": {
                    "tokenTypes": TOKEN_TYPES,
                    "tokenModifiers": TOKEN_MODIFIERS,
                },
                "range": False,
                "full": True,
            },
        }
        log.info("Server capabilities: %s", server_capabilities)
        return server_capabilities

    def m_workspace__execute_command(self, command=None, arguments=()) -> Any:
        if command == "robot.addPluginsDir":
            directory: str = arguments[0]
            assert os.path.isdir(
                directory), f"Expected: {directory} to be a directory."
            self._pm.load_plugins_from(Path(directory))
            return True

        elif command == "robot.getInternalInfo":
            in_memory_docs = []
            workspace = self.workspace
            if workspace:
                for doc in workspace.iter_documents():
                    in_memory_docs.append({"uri": doc.uri})
            return {
                "settings": self.config.get_full_settings(),
                "inMemoryDocs": in_memory_docs,
                "processId": os.getpid(),
            }

        elif command == "robot.resolveInterpreter":
            try:
                from robocorp_ls_core import uris
                from robotframework_ls.ep_resolve_interpreter import (
                    EPResolveInterpreter, )
                from robotframework_ls.ep_resolve_interpreter import IInterpreterInfo

                target_robot: str = arguments[0]

                for ep in self._pm.get_implementations(EPResolveInterpreter):
                    interpreter_info: IInterpreterInfo = ep.get_interpreter_info_for_doc_uri(
                        uris.from_fs_path(target_robot))
                    if interpreter_info is not None:
                        return {
                            "pythonExe":
                            interpreter_info.get_python_exe(),
                            "environ":
                            interpreter_info.get_environ(),
                            "additionalPythonpathEntries":
                            interpreter_info.get_additional_pythonpath_entries(
                            ),
                        }
            except:
                log.exception(
                    f"Error resolving interpreter. Args: {arguments}")

        elif command == "robot.getLanguageServerVersion":
            return __version__

        elif command.startswith("robot.internal.rfinteractive."):
            return rf_interactive_integration.execute_command(
                command, self, self._rf_interpreters_manager, arguments)

        elif command == "robot.listTests":
            doc_uri = arguments[0]["uri"]

            rf_api_client = self._server_manager.get_others_api_client(doc_uri)
            if rf_api_client is not None:
                func = partial(
                    self._async_api_request,
                    rf_api_client,
                    "request_list_tests",
                    doc_uri=doc_uri,
                )
                func = require_monitor(func)
                return func

            log.info("Unable to list tests (no api available).")
            return []

    @overrides(PythonLanguageServer.m_workspace__did_change_configuration)
    @log_and_silence_errors(log)
    def m_workspace__did_change_configuration(self, **kwargs):
        PythonLanguageServer.m_workspace__did_change_configuration(
            self, **kwargs)
        self._server_manager.set_config(self.config)

    # --- Methods to forward to the api

    @overrides(PythonLanguageServer.m_shutdown)
    @log_and_silence_errors(log)
    def m_shutdown(self, **kwargs):
        try:
            from robocorp_ls_core.remote_fs_observer_impl import RemoteFSObserver

            remote_observer = typing.cast(RemoteFSObserver, self._fs_observer)
            remote_observer.dispose()
        except Exception:
            log.exception("Error disposing RemoteFSObserver.")
        self._server_manager.shutdown()

        PythonLanguageServer.m_shutdown(self, **kwargs)

    @overrides(PythonLanguageServer.m_exit)
    @log_and_silence_errors(log)
    def m_exit(self, **kwargs):
        self._server_manager.exit()

        PythonLanguageServer.m_exit(self, **kwargs)

    def m_text_document__formatting(self,
                                    textDocument=None,
                                    options=None) -> Optional[list]:
        doc_uri = textDocument["uri"]

        source_format_rf_api_client = self._server_manager.get_others_api_client(
            doc_uri)
        if source_format_rf_api_client is None:
            log.info("Unable to get API for source format.")
            return []

        message_matcher = source_format_rf_api_client.request_source_format(
            text_document=textDocument, options=options)
        if message_matcher is None:
            raise RuntimeError(
                "Error requesting code formatting (message_matcher==None).")
        curtime = time.time()
        maxtime = curtime + DEFAULT_COMPLETIONS_TIMEOUT

        # i.e.: wait X seconds for the code format and bail out if we
        # can't get it.
        available_time = maxtime - time.time()
        if available_time <= 0:
            raise RuntimeError(
                "Code formatting timed-out (available_time <= 0).")

        if message_matcher.event.wait(available_time):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result
                else:
                    return []
        raise RuntimeError("Code formatting timed-out.")

    @overrides(PythonLanguageServer.m_text_document__did_close)
    def m_text_document__did_close(self, textDocument=None, **_kwargs):
        self._server_manager.forward(
            ("api", "lint", "others"),
            "textDocument/didClose",
            {"textDocument": textDocument},
        )
        PythonLanguageServer.m_text_document__did_close(
            self, textDocument=textDocument, **_kwargs)

    @overrides(PythonLanguageServer.m_text_document__did_open)
    def m_text_document__did_open(self, textDocument=None, **_kwargs):
        self._server_manager.forward(
            ("api", "lint", "others"),
            "textDocument/didOpen",
            {"textDocument": textDocument},
        )
        PythonLanguageServer.m_text_document__did_open(
            self, textDocument=textDocument, **_kwargs)

    @overrides(PythonLanguageServer.m_text_document__did_change)
    def m_text_document__did_change(self,
                                    contentChanges=None,
                                    textDocument=None,
                                    **_kwargs):
        self._server_manager.forward(
            ("api", "lint", "others"),
            "textDocument/didChange",
            {
                "contentChanges": contentChanges,
                "textDocument": textDocument
            },
        )
        PythonLanguageServer.m_text_document__did_change(
            self,
            contentChanges=contentChanges,
            textDocument=textDocument,
            **_kwargs)

    @overrides(PythonLanguageServer.m_workspace__did_change_workspace_folders)
    def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):
        self._server_manager.forward(
            ("api", "lint", "others"),
            "workspace/didChangeWorkspaceFolders",
            {"event": event},
        )
        PythonLanguageServer.m_workspace__did_change_workspace_folders(
            self, event=event, **_kwargs)

    # --- Customized implementation

    @overrides(PythonLanguageServer.lint)
    def lint(self, doc_uri, is_saved) -> None:
        self._lint_manager.schedule_lint(doc_uri, is_saved)

    @overrides(PythonLanguageServer.cancel_lint)
    def cancel_lint(self, doc_uri) -> None:
        self._lint_manager.cancel_lint(doc_uri)

    def m_text_document__completion(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(self._threaded_document_completion, rf_api_client,
                           doc_uri, line, col)
            func = require_monitor(func)
            return func

        log.info("Unable to get completions (no api available).")
        return []

    @log_and_silence_errors(log, return_on_error=[])
    def _threaded_document_completion(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        doc_uri: str,
        line: int,
        col: int,
        monitor: IMonitor,
    ) -> list:
        from robotframework_ls.impl.completion_context import CompletionContext
        from robotframework_ls.impl import section_completions
        from robotframework_ls.impl import snippets_completions
        from robocorp_ls_core.client_base import wait_for_message_matchers

        ws = self.workspace
        if not ws:
            log.critical("Workspace must be set before returning completions.")
            return []

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for completions." %
                         (doc_uri, ))
            return []

        ctx = CompletionContext(document, line, col, config=self.config)
        completions = []

        # Asynchronous completion.
        message_matchers: List[Optional[IIdMessageMatcher]] = []
        message_matchers.append(
            rf_api_client.request_complete_all(doc_uri, line, col))

        # These run locally (no need to get from the server).
        completions.extend(section_completions.complete(ctx))
        completions.extend(snippets_completions.complete(ctx))

        accepted_message_matchers = wait_for_message_matchers(
            message_matchers,
            monitor,
            rf_api_client.request_cancel,
            DEFAULT_COMPLETIONS_TIMEOUT,
        )
        for message_matcher in accepted_message_matchers:
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    completions.extend(result)

        return completions

    @log_and_silence_errors(log)
    def _async_api_request(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        request_method_name: str,
        doc_uri: str,
        monitor: IMonitor,
        **kwargs,
    ):
        from robocorp_ls_core.client_base import wait_for_message_matcher

        func = getattr(rf_api_client, request_method_name)

        ws = self.workspace
        if not ws:
            log.critical("Workspace must be set before calling %s.",
                         request_method_name)
            return None

        document = ws.get_document(doc_uri, accept_from_file=True)
        if document is None:
            log.critical("Unable to find document (%s) for %s." %
                         (doc_uri, request_method_name))
            return None

        # Asynchronous completion.
        message_matcher: Optional[IIdMessageMatcher] = func(doc_uri, **kwargs)
        if message_matcher is None:
            log.debug("Message matcher for %s returned None.",
                      request_method_name)
            return None

        if wait_for_message_matcher(
                message_matcher,
                rf_api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    @log_and_silence_errors(log)
    def _async_api_request_no_doc(
        self,
        rf_api_client: IRobotFrameworkApiClient,
        request_method_name: str,
        monitor: Optional[IMonitor],
        **kwargs,
    ):
        from robocorp_ls_core.client_base import wait_for_message_matcher

        func = getattr(rf_api_client, request_method_name)

        # Asynchronous completion.
        message_matcher: Optional[IIdMessageMatcher] = func(**kwargs)
        if message_matcher is None:
            log.debug("Message matcher for %s returned None.",
                      request_method_name)
            return None

        if wait_for_message_matcher(
                message_matcher,
                rf_api_client.request_cancel,
                DEFAULT_COMPLETIONS_TIMEOUT,
                monitor,
        ):
            msg = message_matcher.msg
            if msg is not None:
                result = msg.get("result")
                if result:
                    return result

        return None

    def m_text_document__definition(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(
                self._async_api_request,
                rf_api_client,
                "request_find_definition",
                doc_uri=doc_uri,
                line=line,
                col=col,
            )
            func = require_monitor(func)
            return func

        log.info("Unable to find definition (no api available).")
        return None

    def m_text_document__signature_help(self, **kwargs):
        """
        "params": {
            "textDocument": {
                "uri": "file:///x%3A/vscode-robot/local_test/Basic/resources/keywords.robot"
            },
            "position": {"line": 7, "character": 22},
            "context": {
                "isRetrigger": False,
                "triggerCharacter": " ",
                "triggerKind": 2,
            },
        },
        """
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]

        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(
                self._async_api_request,
                rf_api_client,
                "request_signature_help",
                doc_uri=doc_uri,
                line=line,
                col=col,
            )
            func = require_monitor(func)
            return func

        log.info("Unable to get signature (no api available).")
        return []

    def m_text_document__folding_range(self, **kwargs):
        """
        "params": {
            "textDocument": {
                "uri": "file:///x%3A/vscode-robot/local_test/Basic/resources/keywords.robot"
            },
        },
        """
        doc_uri = kwargs["textDocument"]["uri"]

        rf_api_client = self._server_manager.get_others_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(
                self._async_api_request,
                rf_api_client,
                "request_folding_range",
                doc_uri=doc_uri,
            )
            func = require_monitor(func)
            return func

        log.info("Unable to get folding range (no api available).")
        return []

    def m_text_document__code_lens(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]

        rf_api_client = self._server_manager.get_others_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(
                self._async_api_request,
                rf_api_client,
                "request_code_lens",
                doc_uri=doc_uri,
            )
            func = require_monitor(func)
            return func

        log.info("Unable to get code lens (no api available).")
        return []

    def m_code_lens__resolve(self, **kwargs):
        code_lens: CodeLensTypedDict = kwargs

        code_lens_command = code_lens.get("command")
        data = code_lens.get("data")
        if code_lens_command is None and isinstance(data, dict):
            # For the interactive shell we need to resolve the arguments.
            uri = data.get("uri")

            rf_api_client = self._server_manager.get_others_api_client(uri)
            if rf_api_client is not None:
                func = partial(
                    self._async_api_request_no_doc,
                    rf_api_client,
                    "request_resolve_code_lens",
                    code_lens=code_lens,
                )
                func = require_monitor(func)
                return func

            log.info("Unable to resolve code lens (no api available).")

        return code_lens

    def m_text_document__document_symbol(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]

        rf_api_client = self._server_manager.get_others_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(
                self._async_api_request,
                rf_api_client,
                "request_document_symbol",
                doc_uri=doc_uri,
            )
            func = require_monitor(func)
            return func

        log.info("Unable to get document symbol (no api available).")
        return []

    def m_text_document__hover(self, **kwargs):
        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line, col = kwargs["position"]["line"], kwargs["position"]["character"]
        rf_api_client = self._server_manager.get_regular_rf_api_client(doc_uri)
        if rf_api_client is not None:
            func = partial(
                self._async_api_request,
                rf_api_client,
                "request_hover",
                doc_uri=doc_uri,
                line=line,
                col=col,
            )
            func = require_monitor(func)
            return func

        log.info("Unable to compute hover (no api available).")
        return []

    def m_text_document__semantic_tokens__range(self,
                                                textDocument=None,
                                                range=None):
        raise RuntimeError("Not currently implemented!")

    def m_text_document__semantic_tokens__full(self, textDocument=None):
        doc_uri = textDocument["uri"]
        api = self._server_manager.get_others_api_client(doc_uri)
        if api is None:
            log.info(
                "Unable to get api client when computing semantic tokens (full)."
            )
            return {"resultId": None, "data": []}

        func = partial(
            self._async_api_request_no_doc,
            api,
            "request_semantic_tokens_full",
            text_document=textDocument,
        )
        func = require_monitor(func)
        return func

    def m_workspace__symbol(self, query: Optional[str] = None) -> Any:
        api = self._server_manager.get_others_api_client("")
        if api is None:
            log.info("Unable to search workspace symbols (no api available).")
            return None

        func = partial(
            self._async_api_request_no_doc,
            api,
            "request_workspace_symbols",
            query=query,
        )
        func = require_monitor(func)
        return func
class RobocorpLanguageServer(PythonLanguageServer):

    # V2: save the account info along to validate user.
    CLOUD_LIST_WORKSPACE_CACHE_KEY = "CLOUD_LIST_WORKSPACE_CACHE_V2"
    PACKAGE_ACCESS_LRU_CACHE_KEY = "PACKAGE_ACCESS_LRU_CACHE"

    def __init__(self, read_stream, write_stream):
        from robocorp_code.rcc import Rcc
        from robocorp_ls_core.cache import DirCache
        from robocorp_ls_core.pluginmanager import PluginManager
        from robocorp_ls_core.ep_providers import DefaultConfigurationProvider
        from robocorp_ls_core.ep_providers import EPConfigurationProvider
        from robocorp_ls_core.ep_providers import DefaultDirCacheProvider
        from robocorp_ls_core.ep_providers import EPDirCacheProvider
        from robocorp_ls_core.ep_providers import DefaultEndPointProvider
        from robocorp_ls_core.ep_providers import EPEndPointProvider

        user_home = os.getenv("ROBOCORP_CODE_USER_HOME", None)
        if user_home is None:
            user_home = os.path.expanduser("~")
        cache_dir = os.path.join(user_home, ".robocorp-code", ".cache")

        log.debug(f"Cache dir: {cache_dir}")

        try:
            import ssl
        except:
            # This means that we won't be able to download drivers to
            # enable the creation of browser locators!
            # Let's print a bit more info.
            env_vars_info = ""

            related_vars = [
                "LD_LIBRARY_PATH",
                "PATH",
                "DYLD_LIBRARY_PATH",
                "DYLD_FALLBACK_LIBRARY_PATH",
            ]
            for v in related_vars:
                libpath = os.environ.get(v, "")

                libpath = "\n    ".join(libpath.split(os.pathsep))
                if libpath:
                    libpath = "\n    " + libpath + "\n"
                else:
                    libpath = " <not set>\n"

                env_vars_info += f"{v}: {libpath}"

            log.critical(f"SSL module could not be imported.\n"
                         f"sys.executable: {sys.executable}\n"
                         f"Env vars info: {env_vars_info}\n")

        self._dir_cache = DirCache(cache_dir)
        self._rcc = Rcc(self)
        self._track = True
        self._local_list_robots_cache: Dict[
            Path, CachedFileInfo[LocalRobotMetadataInfoDict]] = {}
        PythonLanguageServer.__init__(self, read_stream, write_stream)

        self._pm = PluginManager()
        self._config_provider = DefaultConfigurationProvider(self.config)
        self._pm.set_instance(EPConfigurationProvider, self._config_provider)
        self._pm.set_instance(EPDirCacheProvider,
                              DefaultDirCacheProvider(self._dir_cache))
        self._pm.set_instance(EPEndPointProvider,
                              DefaultEndPointProvider(self._endpoint))
        from robocorp_code.plugins.resolve_interpreter import register_plugins

        register_plugins(self._pm)
        self._locators_in_thread_api = _LocatorsInThreadAPI()

    @overrides(PythonLanguageServer.m_initialize)
    def m_initialize(
        self,
        processId=None,
        rootUri=None,
        rootPath=None,
        initializationOptions=None,
        workspaceFolders=None,
        **_kwargs,
    ) -> dict:
        ret = PythonLanguageServer.m_initialize(
            self,
            processId=processId,
            rootUri=rootUri,
            rootPath=rootPath,
            initializationOptions=initializationOptions,
            workspaceFolders=workspaceFolders,
        )

        if initializationOptions and isinstance(initializationOptions, dict):
            self._track = not initializationOptions.get("do-not-track", False)

        from robocorp_code import __version__

        self._feedback_metric("vscode.started", __version__)
        self._feedback_metric("vscode.started.os", sys.platform)
        return ret

    def _feedback_metric(self, name, value="+1"):
        if not self._track:
            return

        from robocorp_ls_core.timeouts import TimeoutTracker

        timeout_tracker = TimeoutTracker.get_singleton()
        timeout_tracker.call_on_timeout(
            0.1, partial(self._rcc.feedack_metric, name, value))

    def m_shutdown(self, **_kwargs):
        self._locators_in_thread_api.shutdown()
        PythonLanguageServer.m_shutdown(self, **_kwargs)

    @overrides(PythonLanguageServer.cancel_lint)
    def cancel_lint(self, doc_uri):
        pass  # no-op

    @overrides(PythonLanguageServer.lint)
    def lint(self, doc_uri, is_saved):
        pass  # no-op

    @overrides(PythonLanguageServer._create_config)
    def _create_config(self) -> IConfig:
        from robocorp_code.robocorp_config import RobocorpConfig

        return RobocorpConfig()

    @overrides(PythonLanguageServer.capabilities)
    def capabilities(self):
        from robocorp_ls_core.lsp import TextDocumentSyncKind
        from robocorp_code.commands import ALL_SERVER_COMMANDS

        server_capabilities = {
            "codeActionProvider": False,
            # "codeLensProvider": {
            #     "resolveProvider": False,  # We may need to make this configurable
            # },
            "completionProvider": {
                "resolveProvider": False  # We know everything ahead of time
            },
            "documentFormattingProvider": False,
            "documentHighlightProvider": False,
            "documentRangeFormattingProvider": False,
            "documentSymbolProvider": False,
            "definitionProvider": False,
            "executeCommandProvider": {
                "commands": ALL_SERVER_COMMANDS
            },
            "hoverProvider": True,
            "referencesProvider": False,
            "renameProvider": False,
            "foldingRangeProvider": False,
            "textDocumentSync": {
                "change": TextDocumentSyncKind.INCREMENTAL,
                "save": {
                    "includeText": False
                },
                "openClose": True,
            },
            "workspace": {
                "workspaceFolders": {
                    "supported": True,
                    "changeNotifications": True
                }
            },
        }
        log.info("Server capabilities: %s", server_capabilities)
        return server_capabilities

    def m_workspace__execute_command(self, command=None, arguments=()) -> Any:
        return command_dispatcher.dispatch(self, command, arguments)

    @command_dispatcher(commands.ROBOCORP_IS_LOGIN_NEEDED_INTERNAL)
    def _is_login_needed_internal(self) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        with progress_context(self._endpoint, "Validating cloud credentials",
                              self._dir_cache):
            login_needed = not self._rcc.credentials_valid()
        return {
            "success": login_needed,
            "message": None,
            "result": login_needed
        }

    @command_dispatcher(commands.ROBOCORP_CLOUD_LOGIN_INTERNAL)
    def _cloud_login(self, params: CloudLoginParamsDict) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.login")

        # When new credentials are added we need to remove existing caches.
        self._dir_cache.discard(self.CLOUD_LIST_WORKSPACE_CACHE_KEY)

        credentials = params["credentials"]
        with progress_context(self._endpoint, "Adding cloud credentials",
                              self._dir_cache):
            result = self._rcc.add_credentials(credentials)
            self._endpoint.notify("$/linkedAccountChanged")
            if not result.success:
                return result.as_dict()

            result = self._rcc.credentials_valid()
        return {"success": result, "message": None, "result": result}

    @command_dispatcher(commands.ROBOCORP_CLOUD_LOGOUT_INTERNAL)
    def _cloud_logout(self) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.logout")

        # When credentials are removed we need to remove existing caches.
        self._dir_cache.discard(self.CLOUD_LIST_WORKSPACE_CACHE_KEY)

        with progress_context(self._endpoint, "Removing cloud credentials",
                              self._dir_cache):
            ret = self._rcc.remove_current_credentials().as_dict()
            self._endpoint.notify("$/linkedAccountChanged")
            return ret

    @command_dispatcher(commands.ROBOCORP_SAVE_IN_DISK_LRU)
    def _save_in_disk_lru(self, params: dict) -> ActionResultDict:
        name = params["name"]
        entry = params["entry"]
        lru_size = params["lru_size"]
        try:
            cache_lru_list = self._dir_cache.load(name, list)
        except:
            cache_lru_list = []

        try:
            if cache_lru_list[0] == entry:
                # Nothing to do as it already matches.
                return {"success": True, "message": "", "result": entry}

            cache_lru_list.remove(entry)
        except:
            pass  # If empty or if entry is not there, just proceed.

        if len(cache_lru_list) >= lru_size:
            cache_lru_list = cache_lru_list[:-1]

        cache_lru_list.insert(0, entry)
        self._dir_cache.store(name, cache_lru_list)
        return {"success": True, "message": "", "result": entry}

    @command_dispatcher(commands.ROBOCORP_LOAD_FROM_DISK_LRU, list)
    def _load_from_disk_lru(self, params: dict) -> ActionResultDict:
        try:
            name = params["name"]
            cache_lru_list = self._dir_cache.load(name, list)
        except:
            cache_lru_list = []

        return cache_lru_list

    def _get_sort_key_info(self):
        try:
            cache_lru_list: List[PackageInfoInLRUDict] = self._dir_cache.load(
                self.PACKAGE_ACCESS_LRU_CACHE_KEY, list)
        except KeyError:
            cache_lru_list = []
        DEFAULT_SORT_KEY = 10
        ws_id_and_pack_id_to_lru_index: Dict = {}
        for i, entry in enumerate(cache_lru_list):

            if i >= DEFAULT_SORT_KEY:
                break

            if isinstance(entry, dict):
                ws_id = entry.get("workspace_id")
                pack_id = entry.get("package_id")
                if ws_id is not None and pack_id is not None:
                    key = (ws_id, pack_id)
                    ws_id_and_pack_id_to_lru_index[key] = i
        return ws_id_and_pack_id_to_lru_index

    @command_dispatcher(commands.ROBOCORP_GET_LINKED_ACCOUNT_INFO_INTERNAL)
    def _get_linked_account_info(self, params=None) -> ActionResultDict:
        from robocorp_code.rcc import AccountInfo

        curr_account_info: Optional[
            AccountInfo] = self._rcc.last_verified_account_info
        if curr_account_info is None:
            curr_account_info = self._rcc.get_valid_account_info()
            if curr_account_info is None:
                return {
                    "success": False,
                    "message":
                    "Unable to get account info (no linked account).",
                    "result": None,
                }
        return {
            "success": True,
            "message": None,
            "result": {
                "account": curr_account_info.account,
                "identifier": curr_account_info.identifier,
                "email": curr_account_info.email,
                "fullname": curr_account_info.fullname,
            },
        }

    @command_dispatcher(commands.ROBOCORP_CLOUD_LIST_WORKSPACES_INTERNAL)
    def _cloud_list_workspaces(
            self,
            params: CloudListWorkspaceDict) -> ListWorkspacesActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        DEFAULT_SORT_KEY = 10
        package_info: PackageInfoDict
        ws_dict: WorkspaceInfoDict

        ws_id_and_pack_id_to_lru_index = self._get_sort_key_info()
        curr_account_info = self._rcc.last_verified_account_info
        if curr_account_info is None:
            curr_account_info = self._rcc.get_valid_account_info()
            if curr_account_info is None:
                return {
                    "success": False,
                    "message":
                    "Unable to get workspace info (no user is logged in).",
                    "result": None,
                }

        account_cache_key = (curr_account_info.account,
                             curr_account_info.identifier)

        if not params.get("refresh", True):
            try:
                cached: ListWorkspaceCachedInfoDict = self._dir_cache.load(
                    self.CLOUD_LIST_WORKSPACE_CACHE_KEY, dict)
            except KeyError:
                pass
            else:
                # We need to update the sort key when it's gotten from the cache.
                try:
                    if account_cache_key == tuple(
                            cached.get("account_cache_key", ())):
                        for ws_dict in cached["ws_info"]:
                            for package_info in ws_dict["packages"]:
                                key = (package_info["workspaceId"],
                                       package_info["id"])
                                sort_key = "%05d%s" % (
                                    ws_id_and_pack_id_to_lru_index.get(
                                        key, DEFAULT_SORT_KEY),
                                    package_info["name"].lower(),
                                )

                                package_info["sortKey"] = sort_key
                        return {
                            "success": True,
                            "message": None,
                            "result": cached["ws_info"],
                        }
                except Exception:
                    log.exception(
                        "Error computing new sort keys for cached entry. Refreshing and proceeding."
                    )

        last_error_result = None

        with progress_context(self._endpoint, "Listing cloud workspaces",
                              self._dir_cache):
            ws: IRccWorkspace
            ret: List[WorkspaceInfoDict] = []
            result = self._rcc.cloud_list_workspaces()
            if not result.success:
                return result.as_dict()

            workspaces = result.result
            for ws in workspaces:
                packages: List[PackageInfoDict] = []

                activity_package: IRccRobotMetadata
                activities_result = self._rcc.cloud_list_workspace_robots(
                    ws.workspace_id)
                if not activities_result.success:
                    # If we can't list the robots of a specific workspace, just skip it
                    # (the log should still show it but we can proceed to list the
                    # contents of other workspaces).
                    last_error_result = activities_result
                    continue

                workspace_activities = activities_result.result
                for activity_package in workspace_activities:

                    key = (ws.workspace_id, activity_package.robot_id)
                    sort_key = "%05d%s" % (
                        ws_id_and_pack_id_to_lru_index.get(
                            key, DEFAULT_SORT_KEY),
                        activity_package.robot_name.lower(),
                    )

                    package_info = {
                        "name": activity_package.robot_name,
                        "id": activity_package.robot_id,
                        "sortKey": sort_key,
                        "workspaceId": ws.workspace_id,
                        "workspaceName": ws.workspace_name,
                    }
                    packages.append(package_info)

                ws_dict = {
                    "workspaceName": ws.workspace_name,
                    "workspaceId": ws.workspace_id,
                    "packages": packages,
                }
                ret.append(ws_dict)

        if not ret and last_error_result is not None:
            return last_error_result.as_dict()

        if ret:  # Only store if we got something.
            store: ListWorkspaceCachedInfoDict = {
                "ws_info": ret,
                "account_cache_key": account_cache_key,
            }
            self._dir_cache.store(self.CLOUD_LIST_WORKSPACE_CACHE_KEY, store)
        return {"success": True, "message": None, "result": ret}

    @command_dispatcher(commands.ROBOCORP_CREATE_ROBOT_INTERNAL)
    def _create_robot(self, params: CreateRobotParamsDict) -> ActionResultDict:
        self._feedback_metric("vscode.create.robot")
        directory = params["directory"]
        template = params["template"]
        name = params["name"]

        return self._rcc.create_robot(template, os.path.join(directory,
                                                             name)).as_dict()

    @command_dispatcher(commands.ROBOCORP_LIST_ROBOT_TEMPLATES_INTERNAL)
    def _list_activity_templates(self, params=None) -> ActionResultDict:
        result = self._rcc.get_template_names()
        return result.as_dict()

    def _get_robot_metadata(
        self,
        sub: Path,
        curr_cache: Dict[Path, CachedFileInfo[LocalRobotMetadataInfoDict]],
        new_cache: Dict[Path, CachedFileInfo[LocalRobotMetadataInfoDict]],
    ) -> Optional[LocalRobotMetadataInfoDict]:
        """
        Note that we get the value from the current cache and then put it in
        the new cache if it's still valid (that way we don't have to mutate
        the old cache to remove stale values... all that's valid is put in
        the new cache).
        """
        robot_yaml = sub / "robot.yaml"

        cached_file_info: Optional[
            CachedFileInfo[LocalRobotMetadataInfoDict]] = curr_cache.get(sub)
        if cached_file_info is not None:
            if cached_file_info.is_cache_valid():
                new_cache[sub] = cached_file_info
                return cached_file_info.value

        if robot_yaml.exists():
            from robocorp_ls_core import yaml_wrapper

            try:

                def get_robot_metadata(robot_yaml: Path):
                    name = robot_yaml.parent.name
                    with robot_yaml.open("r", encoding="utf-8") as stream:
                        yaml_contents = yaml_wrapper.load(stream)
                        name = yaml_contents.get("name", name)

                    robot_metadata: LocalRobotMetadataInfoDict = {
                        "directory": str(sub),
                        "filePath": str(robot_yaml),
                        "name": name,
                        "yamlContents": yaml_contents,
                    }
                    return robot_metadata

                cached_file_info = new_cache[sub] = CachedFileInfo(
                    robot_yaml, get_robot_metadata)
                return cached_file_info.value

            except:
                log.exception(
                    f"Unable to get load robot metadata for: {robot_yaml}")

        return None

    @command_dispatcher(commands.ROBOCORP_RUN_IN_RCC_INTERNAL)
    def _run_in_rcc_internal(self,
                             params=RunInRccParamsDict) -> ActionResultDict:
        try:
            args = params["args"]
            ret = self._rcc._run_rcc(args)
        except Exception as e:
            log.exception(f"Error running in RCC: {params}.")
            return dict(success=False, message=str(e), result=None)
        return ret.as_dict()

    @command_dispatcher(commands.ROBOCORP_LOCAL_LIST_ROBOTS_INTERNAL)
    def _local_list_robots(self,
                           params=None) -> ActionResultDictLocalRobotMetadata:
        curr_cache = self._local_list_robots_cache
        new_cache: Dict[Path, CachedFileInfo[LocalRobotMetadataInfoDict]] = {}

        ret: List[LocalRobotMetadataInfoDict] = []
        try:
            ws = self.workspace
            if ws:
                for folder_path in ws.get_folder_paths():
                    # Check the root directory itself for the robot.yaml.
                    p = Path(folder_path)
                    robot_metadata = self._get_robot_metadata(
                        p, curr_cache, new_cache)
                    if robot_metadata is not None:
                        ret.append(robot_metadata)
                    elif p.is_dir():
                        for sub in p.iterdir():
                            robot_metadata = self._get_robot_metadata(
                                sub, curr_cache, new_cache)
                            if robot_metadata is not None:
                                ret.append(robot_metadata)

            ret.sort(key=lambda dct: dct["name"])
        except Exception as e:
            log.exception("Error listing robots.")
            return dict(success=False, message=str(e), result=None)
        finally:
            # Set the new cache after we finished computing all entries.
            self._local_list_robots_cache = new_cache

        return dict(success=True, message=None, result=ret)

    def _validate_directory(self, directory) -> Optional[str]:
        if not os.path.exists(directory):
            return f"Expected: {directory} to exist."

        if not os.path.isdir(directory):
            return f"Expected: {directory} to be a directory."
        return None

    def _add_package_info_to_access_lru(self, workspace_id, package_id,
                                        directory):
        import time

        try:
            lst: List[PackageInfoInLRUDict] = self._dir_cache.load(
                self.PACKAGE_ACCESS_LRU_CACHE_KEY, list)
        except KeyError:
            lst = []

        new_lst: List[PackageInfoInLRUDict] = [{
            "workspace_id": workspace_id,
            "package_id": package_id,
            "directory": directory,
            "time": time.time(),
        }]
        for i, entry in enumerate(lst):
            if isinstance(entry, dict):
                if (entry.get("package_id") == package_id
                        and entry.get("workspace_id") == workspace_id):
                    continue  # Skip this one (we moved it to the start of the LRU).
                new_lst.append(entry)

            if i == 5:
                break  # Max number of items in the LRU reached.

        self._dir_cache.store(self.PACKAGE_ACCESS_LRU_CACHE_KEY, new_lst)

    @command_dispatcher(commands.ROBOCORP_UPLOAD_TO_EXISTING_ROBOT_INTERNAL)
    def _upload_to_existing_activity(
            self, params: UploadRobotParamsDict) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.upload.existing")

        directory = params["directory"]
        error_msg = self._validate_directory(directory)
        if error_msg:
            return {"success": False, "message": error_msg, "result": None}

        workspace_id = params["workspaceId"]
        robot_id = params["robotId"]
        with progress_context(self._endpoint, "Uploading to existing robot",
                              self._dir_cache):

            result = self._rcc.cloud_set_robot_contents(
                directory, workspace_id, robot_id)
            self._add_package_info_to_access_lru(workspace_id, robot_id,
                                                 directory)

        return result.as_dict()

    @command_dispatcher(commands.ROBOCORP_UPLOAD_TO_NEW_ROBOT_INTERNAL)
    def _upload_to_new_robot(
            self, params: UploadNewRobotParamsDict) -> ActionResultDict:
        from robocorp_ls_core.progress_report import progress_context

        self._feedback_metric("vscode.cloud.upload.new")

        directory = params["directory"]
        error_msg = self._validate_directory(directory)
        if error_msg:
            return {"success": False, "message": error_msg, "result": None}

        workspace_id = params["workspaceId"]
        robot_name = params["robotName"]

        # When we upload to a new activity, clear the existing cache key.
        self._dir_cache.discard(self.CLOUD_LIST_WORKSPACE_CACHE_KEY)
        with progress_context(self._endpoint, "Uploading to new robot",
                              self._dir_cache):
            new_robot_result = self._rcc.cloud_create_robot(
                workspace_id, robot_name)
            if not new_robot_result.success:
                return new_robot_result.as_dict()

            robot_id = new_robot_result.result
            if not robot_id:
                return dict(
                    success=False,
                    message=
                    "Expected to have package id from creating new activity.",
                    result=None,
                )

            result = self._rcc.cloud_set_robot_contents(
                directory, workspace_id, robot_id)
            self._add_package_info_to_access_lru(workspace_id, robot_id,
                                                 directory)
        return result.as_dict()

    @command_dispatcher(commands.ROBOCORP_GET_PLUGINS_DIR, str)
    def _get_plugins_dir(self, params=None) -> str:
        return str(Path(__file__).parent / "plugins")

    @command_dispatcher(
        commands.ROBOCORP_COMPUTE_ROBOT_LAUNCH_FROM_ROBOCORP_CODE_LAUNCH)
    def _compute_robot_launch_from_robocorp_code_launch(
            self, params: dict) -> ActionResultDictRobotLaunch:
        from robocorp_code import compute_launch

        name: Optional[str] = params.get("name")
        request: Optional[str] = params.get("request")
        task: Optional[str] = params.get("task")
        robot: Optional[str] = params.get("robot")
        additional_pythonpath_entries: Optional[List[str]] = params.get(
            "additionalPythonpathEntries")
        env: Optional[Dict[str, str]] = params.get("env")
        python_exe: Optional[str] = params.get("pythonExe")

        return compute_launch.compute_robot_launch_from_robocorp_code_launch(
            name, request, task, robot, additional_pythonpath_entries, env,
            python_exe)

    @command_dispatcher(commands.ROBOCORP_RESOLVE_INTERPRETER, dict)
    def _resolve_interpreter(self, params=None) -> ActionResultDict:
        from robocorp_ls_core.ep_resolve_interpreter import EPResolveInterpreter
        from robocorp_ls_core.ep_resolve_interpreter import IInterpreterInfo

        try:
            from robocorp_ls_core import uris

            target_robot: str = params.get("target_robot")

            for ep in self._pm.get_implementations(EPResolveInterpreter):
                interpreter_info: IInterpreterInfo = (
                    ep.get_interpreter_info_for_doc_uri(
                        uris.from_fs_path(target_robot)))
                if interpreter_info is not None:
                    return {
                        "success": True,
                        "message": None,
                        "result": {
                            "pythonExe":
                            interpreter_info.get_python_exe(),
                            "environ":
                            interpreter_info.get_environ(),
                            "additionalPythonpathEntries":
                            interpreter_info.get_additional_pythonpath_entries(
                            ),
                        },
                    }
        except Exception as e:
            log.exception(f"Error resolving interpreter. Args: {params}")
            return {"success": False, "message": str(e), "result": None}

        # i.e.: no error but we couldn't find an interpreter.
        return {"success": True, "message": "", "result": None}

    @command_dispatcher(
        commands.ROBOCORP_START_BROWSER_LOCATOR_INTERNAL,
        expected_return_cls=(Future, dict),
    )
    def _start_browser_locator(
        self,
        params: dict = None
    ) -> "Union[ActionResultDict, Future[ActionResultDict]]":
        if not params or "robotYaml" not in params:
            self._locators_in_thread_api.set_robot_yaml_location("")
            return {
                "success": False,
                "message": "robot.yaml filename not passed",
                "result": None,
            }

        robot_yaml = params["robotYaml"]
        if not os.path.exists(robot_yaml):
            return {
                "success": False,
                "message": f"robot.yaml {robot_yaml} does not exist.",
                "result": None,
            }

        return self._locators_in_thread_api.start_browser_locator(
            robot_yaml=robot_yaml)

    @command_dispatcher(commands.ROBOCORP_STOP_BROWSER_LOCATOR,
                        expected_return_cls=Future)
    def _stop_browser_locator(self,
                              params: dict = None
                              ) -> "Future[ActionResultDict]":
        return self._locators_in_thread_api.browser_locator_stop()

    @command_dispatcher(
        commands.ROBOCORP_CREATE_LOCATOR_FROM_BROWSER_PICK_INTERNAL,
        expected_return_cls=Future,
    )
    def _create_locator_fom_browser_pick(self,
                                         params: dict = None
                                         ) -> "Future[ActionResultDict]":
        self._feedback_metric("vscode.locator.created", "browser")
        return self._locators_in_thread_api.create_locator_from_browser_pick()

    @command_dispatcher(
        commands.ROBOCORP_CREATE_LOCATOR_FROM_SCREEN_REGION_INTERNAL,
        expected_return_cls=(Future, dict),
    )
    def _create_locator_fom_screenshot_pick(
        self,
        params: dict = None
    ) -> Union["Future[ActionResultDict]", ActionResultDict]:
        if not params or "robotYaml" not in params:
            self._locators_in_thread_api.set_robot_yaml_location("")
            return {
                "success": False,
                "message": "robot.yaml filename not passed",
                "result": None,
            }

        robot_yaml = params["robotYaml"]
        if not os.path.exists(robot_yaml):
            return {
                "success": False,
                "message": f"robot.yaml {robot_yaml} does not exist.",
                "result": None,
            }

        self._feedback_metric("vscode.locator.created", "image")
        return self._locators_in_thread_api.create_locator_from_screenshot_pick(
            robot_yaml)

    @command_dispatcher(commands.ROBOCORP_SEND_METRIC)
    def _send_metric(self, params: dict) -> ActionResultDict:
        name = params.get("name")
        value = params.get("value")
        if name is None or value is None:
            return {
                "success": False,
                "message":
                f"Expected name and value. Received name: {name!r} value: {value!r}",
                "result": None,
            }

        self._feedback_metric(name, value)
        return {"success": True, "message": None, "result": None}

    def m_text_document__hover(self, **kwargs):
        """
        When hovering over a png in base64 surrounded by double-quotes... something as:
        "iVBORw0KGgo...rest of png in base 64 contents..."

        i.e.: Provide the contents in markdown format to show the actual image from the
        locators.json.
        """
        from robocorp_ls_core import uris
        from robocorp_ls_core.protocols import IDocument
        from robocorp_ls_core.protocols import IDocumentSelection
        from robocorp_ls_core.lsp import Range
        from robocorp_ls_core.lsp import MarkupKind
        from robocorp_ls_core.lsp import MarkupContent

        doc_uri = kwargs["textDocument"]["uri"]
        # Note: 0-based
        line: int = kwargs["position"]["line"]
        col: int = kwargs["position"]["character"]
        if not uris.to_fs_path(doc_uri).endswith("locators.json"):
            return None
        document: IDocument = self._workspace.get_document(
            doc_uri, accept_from_file=True)
        sel: IDocumentSelection = document.selection(line, col)
        current_line: str = sel.current_line
        i: int = current_line.find(
            '"iVBORw0KGgo'
        )  # I.e.: pngs in base64 always start with this prefix.
        if i >= 0:
            current_line = current_line[i + 1:]
            i = current_line.find('"')
            if i >= 0:
                current_line = current_line[0:i]
                image_path = f"data:image/png;base64,{current_line}"
                s = f"![Screenshot]({image_path})"
                return {
                    "contents": MarkupContent(MarkupKind.Markdown,
                                              s).to_dict(),
                    "range": Range((line, col), (line, col)).to_dict(),
                }

        # Could not find a base-64 img embedded, let's see if we have an element
        # with a relative path.
        import re

        p = Path(document.path).parent

        for found in re.findall('"(.+?)"', current_line):
            if found.endswith(".png"):
                check = p / found
                if check.exists():
                    as_uri = uris.from_fs_path(str(check))
                    s = f"![Screenshot]({as_uri})"
                    return {
                        "contents": MarkupContent(MarkupKind.Markdown,
                                                  s).to_dict(),
                        "range": Range((line, col), (line, col)).to_dict(),
                    }

        return None

    def _get_line_col(self, name, content_lines):
        """
        Note: there are Python libraries that can be used to extract line/col from json information:
        https://pypi.org/project/dirtyjson/
        https://pypi.org/project/json-cfg/ (jsoncfg.node_location(node)).

        So, we could use the json parsing with this, but there's some logic in
        the LocatorsDatabase to deal with old formats and we may have to deal with
        old formats too in this case... given that, for now let's just see if we
        match a substring there (it's very inefficient, but we don't expect
        thousands of locators, so, it should be ok).
        """
        match = f'"{name}"'
        for i, line in enumerate(content_lines):
            col = line.find(match)
            if col >= 0:
                return i, col

        return 0, 0  # I.e.: unable to find

    @command_dispatcher(commands.ROBOCORP_GET_LOCATORS_JSON_INFO)
    def _get_locators_json_info(self,
                                params: dict = None
                                ) -> ActionResultDictLocatorsJsonInfo:
        from RPA.core.locators.database import LocatorsDatabase
        from RPA.core.locators.containers import Locator

        if not params or "robotYaml" not in params:
            return {
                "success": False,
                "message": "robot.yaml filename not passed",
                "result": None,
            }

        path = Path(params["robotYaml"])
        locators_json = path.parent / "locators.json"
        locators_json_info: List[LocatorEntryInfoDict] = []
        locator: Locator
        if locators_json.exists():
            with locators_json.open("r") as stream:
                contents = stream.read()
            content_lines = contents.splitlines()

            try:
                db = LocatorsDatabase(str(locators_json))
                db.load()
                if not db.locators.items():
                    error = db.error
                    if not isinstance(error, str):
                        if isinstance(error, tuple) and len(error) == 2:
                            try:
                                error = error[0] % error[1]
                            except:
                                error = str(error)
                        else:
                            error = str(error)

                    return {"success": False, "message": error, "result": None}

                for name, locator in db.locators.items():
                    as_dict = locator.to_dict()
                    line, col = self._get_line_col(name, content_lines)
                    locators_json_info.append({
                        "name": name,
                        "line": line,
                        "column": col,
                        "type": as_dict["type"],
                        "filePath": str(locators_json),
                    })
            except Exception as e:
                log.exception(f"Error loading locators from: {locators_json}")
                return {"success": False, "message": str(e), "result": None}

        return {"success": True, "message": None, "result": locators_json_info}