# -*- coding: utf-8 -*- """Installable VS Code application description.""" __all__ = ['VSCodeApp'] import datetime import json import os import typing from anaconda_navigator.api.external_apps.base import BaseInstallableApp from anaconda_navigator.api.conda_api import CondaAPI, get_pyscript, get_pyexec from anaconda_navigator import config as navigator_config from anaconda_navigator.static import images from anaconda_navigator.utils.conda import launch as conda_launch_utils from anaconda_navigator.utils.logs import logger from . import detectors if typing.TYPE_CHECKING: import typing_extensions from anaconda_navigator.api import process from anaconda_navigator.config import user as user_config def check_version( parent: typing.Iterator[detectors.DetectedApplication], ) -> typing.Iterator[detectors.DetectedApplication]: """Detect version of the VS Code application.""" application: detectors.DetectedApplication for application in parent: if not application.executable: continue stdout: str stdout, _, _ = conda_launch_utils.run_process([application.executable, '--version']) if stdout: yield application.replace(version=stdout.splitlines()[0]) else: yield application.replace(version='unknown') class VSCodeApp(BaseInstallableApp): """Microsoft VS Code application.""" def __init__(self, process_api: 'process.WorkerManager', config: 'user_config.UserConfig') -> None: """Initialize new :class:`~VSCodeApp` instance.""" detector: 'typing_extensions.Final[detectors.Source]' = detectors.Group( detectors.Group( detectors.CheckConfiguredRoots('vscode_path', configuration=config), detectors.mac_only(), detectors.check_known_mac_roots('Visual Studio Code.app'), detectors.AppendExecutable(os.path.join('Contents', 'Resources', 'app', 'bin', 'code')), ), detectors.Group( detectors.CheckConfiguredRoots('vscode_path', configuration=config), detectors.linux_only(), detectors.CheckKnownRoots( detectors.join(detectors.Linux.root, 'usr', 'share', 'code'), detectors.join(detectors.Linux.root, 'snap', 'code', 'current', 'usr', 'share', 'code'), detectors.join( detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'code', 'current', 'usr', 'share', 'code', ), ), detectors.AppendExecutable(os.path.join('bin', 'code')), ), detectors.Group( detectors.CheckConfiguredRoots('vscode_path', configuration=config), detectors.win_only(), detectors.CheckKnownRoots( detectors.join(detectors.Win.program_files_x86, 'Microsoft VS Code'), detectors.join(detectors.Win.program_files_x64, 'Microsoft VS Code'), detectors.join(detectors.Win.local_app_data, 'Programs', 'Microsoft VS Code'), ), detectors.AppendExecutable(os.path.join('bin', 'code.cmd')) ), check_version, ) super().__init__( app_name='vscode', display_name='VS Code', description=( 'Streamlined code editor with support for development operations like debugging, task running and ' 'version control.' ), image_path=images.VSCODE_ICON_1024_PATH, detector=detector, is_available=True, process_api=process_api, config=config, extra_arguments=('--user-data-dir', os.path.join(navigator_config.CONF_PATH, 'Code')) ) def install_extensions(self) -> 'process.ProcessWorker': """Install app extensions.""" if self.executable is None: return self._process_api.create_process_worker(['python', '--version']) cmd: typing.Sequence[str] = [ self.executable, '--install-extension', 'ms-python.anaconda-extension-pack', # 'ms-python-anaconda-extension', # 'ms-python.python', ] return self._process_api.create_process_worker(cmd) def update_config(self, prefix: str) -> None: """Update user config to use selected Python prefix interpreter.""" try: _config_dir: str = os.path.join(navigator_config.CONF_PATH, 'Code', 'User') _config: str = os.path.join(_config_dir, 'settings.json') try: os.makedirs(_config_dir, exist_ok=True) except OSError as exception: logger.error(exception) return stream: typing.TextIO config_data: typing.Dict[str, typing.Any] if os.path.isfile(_config): try: with open(_config, 'rt', encoding='utf-8') as stream: data = stream.read() self.create_config_backup(data) config_data = json.loads(data) except Exception: # pylint: disable=broad-except return else: config_data = {} config_data.update({ 'python.terminal.activateEnvironment': True, 'python.pythonPath': get_pyexec(prefix), 'python.defaultInterpreterPath': get_pyexec(prefix), 'python.condaPath': get_pyscript(CondaAPI().ROOT_PREFIX, 'conda'), }) with open(_config, 'wt', encoding='utf-8') as stream: json.dump(config_data, stream, sort_keys=True, indent=4) except Exception as exception: # pylint: disable=broad-except logger.error(exception) return @staticmethod def create_config_backup(data: str) -> None: """ Create a backup copy of the app configuration file `data`. Leave only the last 10 backups. """ date: str = datetime.datetime.now().strftime('%Y%m%d%H%M%S') _config_dir: str = os.path.join(navigator_config.CONF_PATH, 'Code', 'User') _config_bck: str = os.path.join(_config_dir, f'bck.{date}.navigator.settings.json') # Make the backup stream: typing.TextIO with open(_config_bck, 'wt', encoding='utf-8') as stream: stream.write(data) # Only keep the latest 10 backups files: typing.List[str] = [ os.path.join(_config_dir, item) for item in os.listdir(_config_dir) if item.startswith('bck.') and item.endswith('.navigator.settings.json') ] path: str for path in sorted(files, reverse=True)[10:]: try: os.remove(path) except OSError: pass