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)
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)
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"
def test_inject(): from robocorp_ls_core.pluginmanager import inject pm = PluginManager() pm.register(EPFoo, FooImpl, keep_instance=True) @inject(foo=EPFoo) def m1(foo, pm): return foo assert m1(pm=pm) == pm.get_instance(EPFoo)
def test_inject_class(): from robocorp_ls_core.pluginmanager import inject pm = PluginManager() pm.register(EPFoo, FooImpl, keep_instance=True) pm.register(EPBar, FooImpl, keep_instance=False) pm.register(EPBar, AnotherFooImpl, keep_instance=False) @inject(foo=EPFoo, foo2=[EPBar]) def m1(foo, foo2, pm): return foo, foo2 assert m1(pm=pm)[0] == pm.get_instance(EPFoo) assert len(m1(pm=pm)[1]) == 2
def test_resolve_interpreter( cases: CasesFixture, config_provider: IConfigProvider, rcc_conda_installed ) -> None: from robocorp_ls_core.constants import NULL from robocorp_code.plugins.resolve_interpreter import RobocorpResolveInterpreter from robocorp_ls_core import uris from robocorp_ls_core.pluginmanager import PluginManager from pathlib import Path from robocorp_code.plugins.resolve_interpreter import _CacheInfo from robocorp_ls_core.ep_providers import EPConfigurationProvider from robocorp_ls_core.ep_providers import EPEndPointProvider _CacheInfo._cache_hit_files = 0 pm = PluginManager() pm.set_instance(EPConfigurationProvider, config_provider) pm.set_instance(EPEndPointProvider, NULL) resolve_interpreter = RobocorpResolveInterpreter(weak_pm=weakref.ref(pm)) path = cases.get_path( "custom_envs/simple-web-scraper/tasks/simple-web-scraper.robot" ) interpreter_info = resolve_interpreter.get_interpreter_info_for_doc_uri( uris.from_fs_path(path) ) assert interpreter_info assert os.path.exists(interpreter_info.get_python_exe()) environ = interpreter_info.get_environ() assert environ assert environ["RPA_SECRET_MANAGER"] == "RPA.Robocloud.Secrets.FileSecrets" assert environ["RPA_SECRET_FILE"] == "/Users/<your-username-here>/vault.json" additional_pythonpath_entries = interpreter_info.get_additional_pythonpath_entries() assert len(additional_pythonpath_entries) == 3 found = set() for v in additional_pythonpath_entries: p = Path(v) assert p.is_dir() found.add(p.name) assert found == {"variables", "libraries", "resources"} assert _CacheInfo._cache_hit_files == 0 assert _CacheInfo._cache_hit_interpreter == 0 interpreter_info = resolve_interpreter.get_interpreter_info_for_doc_uri( uris.from_fs_path(path) ) assert _CacheInfo._cache_hit_files == 3 assert _CacheInfo._cache_hit_interpreter == 1
def pm(): from robocorp_ls_core.pluginmanager import PluginManager p = PluginManager() p.register(EPResolveInterpreter, ResolveInterpreterInTest) return p
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}
def register_plugins(pm: PluginManager): pm.register(EPFoo, FooExt)
def register_plugins(pm: PluginManager): pm.register( EPResolveInterpreter, RobocorpResolveInterpreter, kwargs={"weak_pm": weakref.ref(pm)}, )
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 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
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)
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
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()
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}
def register_plugins(pm: PluginManager): pm.register(EPResolveInterpreter, ResolveInterpreterInTests)