This commit improves collection file editing and error detection directly in the IDE. It adds YAML schema, IDE configuration and automatic tests to validate it. - Introduce a YAML schema for collection file. - Use `yaml-language-server` for enhanced YAML support in VSCode. - Add telemetry disabling in `configure_vscode.py` to respect user privacy. - Add automated checks to validate YAML file structure against the schema. - Remove unused properties and do not allow them in compiler.
191 lines
7.7 KiB
Python
Executable File
191 lines
7.7 KiB
Python
Executable File
"""
|
|
Description:
|
|
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
|
development and installs recommended extensions from '.vscode/extensions.json'.
|
|
|
|
Usage:
|
|
python3 ./scripts/configure_vscode.py
|
|
"""
|
|
# pylint: disable=missing-function-docstring
|
|
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import re
|
|
from typing import Any, Optional
|
|
from shutil import which
|
|
|
|
VSCODE_SETTINGS_JSON_FILE: str = '.vscode/settings.json'
|
|
VSCODE_EXTENSIONS_JSON_FILE: str = '.vscode/extensions.json'
|
|
|
|
def main() -> None:
|
|
ensure_vscode_directory_exists()
|
|
ensure_setting_file_exists()
|
|
add_or_update_settings()
|
|
install_recommended_extensions()
|
|
|
|
def ensure_vscode_directory_exists() -> None:
|
|
vscode_directory_path = os.path.dirname(VSCODE_SETTINGS_JSON_FILE)
|
|
try:
|
|
os.makedirs(vscode_directory_path, exist_ok=True)
|
|
print_success(f"Created or verified directory: {vscode_directory_path}")
|
|
except OSError as error:
|
|
print_error(f"Error handling directory {vscode_directory_path}: {error}")
|
|
|
|
def ensure_setting_file_exists() -> None:
|
|
try:
|
|
if os.path.isfile(VSCODE_SETTINGS_JSON_FILE):
|
|
print_success(f"VSCode settings file exists: {VSCODE_SETTINGS_JSON_FILE}")
|
|
return
|
|
with open(VSCODE_SETTINGS_JSON_FILE, 'w', encoding='utf-8') as file:
|
|
json.dump({}, file, indent=4)
|
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
|
except IOError as error:
|
|
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
|
|
|
def add_or_update_settings() -> None:
|
|
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
|
# Set ESLint validation for specific file types.
|
|
# Details: # pylint: disable-next=line-too-long
|
|
# - https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
|
|
|
|
configure_setting_key('terminal.integrated.env.linux', {"GTK_PATH": ""})
|
|
# Unset GTK_PATH on Linux for Electron development in sandboxed environments
|
|
# like Snap or Flatpak VSCode installations, enabling script execution.
|
|
# Details: # pylint: disable-next=line-too-long
|
|
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
|
|
|
# Disable telemetry
|
|
configure_setting_key('redhat.telemetry.enabled', False)
|
|
configure_setting_key('gitlens.telemetry.enabled', False)
|
|
|
|
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
|
try:
|
|
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
|
settings: dict = json.load(file)
|
|
if configuration_key in settings:
|
|
actual_value = settings[configuration_key]
|
|
if actual_value == desired_value:
|
|
print_skip(f"Already configured as desired: \"{configuration_key}\"")
|
|
return
|
|
settings[configuration_key] = desired_value
|
|
file.seek(0)
|
|
json.dump(settings, file, indent=4)
|
|
file.truncate()
|
|
print_success(f"Added or updated configuration: {configuration_key}")
|
|
except json.JSONDecodeError:
|
|
print_error(f"Failed to update JSON for key {configuration_key}.")
|
|
|
|
def install_recommended_extensions() -> None:
|
|
if not os.path.isfile(VSCODE_EXTENSIONS_JSON_FILE):
|
|
print_error(
|
|
f"The extensions.json file does not exist in the path: {VSCODE_EXTENSIONS_JSON_FILE}."
|
|
)
|
|
return
|
|
with open(VSCODE_EXTENSIONS_JSON_FILE, 'r', encoding='utf-8') as file:
|
|
json_content: str = remove_json_comments(file.read())
|
|
try:
|
|
data: dict = json.loads(json_content)
|
|
extensions: list[str] = data.get("recommendations", [])
|
|
if not extensions:
|
|
print_skip(f"No recommendations found in the {VSCODE_EXTENSIONS_JSON_FILE} file.")
|
|
return
|
|
vscode_cli_path = locate_vscode_cli()
|
|
if vscode_cli_path is None:
|
|
print_error('Visual Studio Code CLI (`code`) tool not found.')
|
|
return
|
|
install_vscode_extensions(vscode_cli_path, extensions)
|
|
except json.JSONDecodeError:
|
|
print_error(f"Invalid JSON in {VSCODE_EXTENSIONS_JSON_FILE}")
|
|
|
|
def locate_vscode_cli() -> Optional[str]:
|
|
vscode_alias = which('code') # More reliable than using `code`, especially on Windows.
|
|
if vscode_alias:
|
|
return vscode_alias
|
|
potential_vscode_cli_paths = [
|
|
# VS Code on macOS may not register 'code' command in PATH
|
|
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
|
|
]
|
|
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
|
if Path(vscode_cli_candidate_path).is_file():
|
|
return vscode_cli_candidate_path
|
|
return None
|
|
|
|
def remove_json_comments(json_like: str) -> str:
|
|
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
|
return re.sub(
|
|
pattern,
|
|
lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE,
|
|
)
|
|
|
|
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
|
successful_installations = 0
|
|
for ext in extensions:
|
|
try:
|
|
result = subprocess.run(
|
|
[vscode_cli_path, "--install-extension", ext],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if "already installed" in result.stdout:
|
|
print_skip(f"Created or verified directory: {ext}")
|
|
else:
|
|
print_success(f"Installed extension: {ext}")
|
|
successful_installations += 1
|
|
print_subprocess_output(result)
|
|
except subprocess.CalledProcessError as e:
|
|
print_subprocess_output(e)
|
|
print_error(f"Failed to install extension: {ext}")
|
|
except FileNotFoundError:
|
|
print_error(' '.join([
|
|
f"Visual Studio Code CLI tool not found: {vscode_cli_path}."
|
|
f"Could not install extension: {ext}",
|
|
]))
|
|
except Exception as e: # pylint: disable=broad-except
|
|
print_error(' '.join([
|
|
f"Failed to install extension '{ext}'.",
|
|
f"Attempted using Visual Studio Code CLI at: '{vscode_cli_path}'.",
|
|
f"Encountered error: {e}",
|
|
]))
|
|
total_extensions = len(extensions)
|
|
print_installation_results(successful_installations, total_extensions)
|
|
|
|
def print_subprocess_output(result: subprocess.CompletedProcess[str]) -> None:
|
|
output = '\n'.join([text.strip() for text in [result.stdout, result.stderr] if text])
|
|
if not output:
|
|
return
|
|
formatted_output = '\t' + output.strip().replace('\n', '\n\t')
|
|
print(formatted_output)
|
|
|
|
def print_installation_results(successful_installations: int, total_extensions: int) -> None:
|
|
if successful_installations == total_extensions:
|
|
print_success(
|
|
f"Successfully installed or verified all {total_extensions} recommended extensions."
|
|
)
|
|
elif successful_installations > 0:
|
|
print_warning(
|
|
f"Partially successful: Installed or verified {successful_installations} "
|
|
f"out of {total_extensions} recommended extensions."
|
|
)
|
|
else:
|
|
print_error("Failed to install any of the recommended extensions.")
|
|
|
|
def print_error(message: str) -> None:
|
|
print(f"[ERROR] {message}", file=sys.stderr)
|
|
|
|
def print_success(message: str) -> None:
|
|
print(f"[SUCCESS] {message}")
|
|
|
|
def print_skip(message: str) -> None:
|
|
print(f"[SKIPPED] {message}")
|
|
|
|
def print_warning(message: str) -> None:
|
|
print(f"[WARNING] {message}", file=sys.stderr)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|