1# Copyright 2022 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Configure Visual Studio Code (VSC) for Pigweed projects. 15 16VSC recognizes three sources of configurable settings: 17 181. Project settings, stored in {project root}/.vscode/settings.json 192. Workspace settings, stored in (workspace root)/.vscode/settings.json; 20 a workspace is a collection of projects/repositories that are worked on 21 together in a single VSC instance 223. The user's personal settings, which are stored somewhere in the user's home 23 directory, and are applied to all instances of VSC 24 25This provides three levels of settings cascading: 26 27 Workspace <- Project <- User 28 29... where the arrow indicates the ability to override. 30 31Out of these three, only project settings are useful to Pigweed projects. User 32settings are essentially global and outside the scope of Pigweed. Workspaces are 33seldom used and don't work well with the Pigweed directory structure. 34 35Nonetheless, we want a three-tiered settings structure for Pigweed projects too: 36 37A. Default settings provided by Pigweed, configuring VSC to use IDE features 38B. Project-level overrides that downstream projects may define 39C. User-level overrides that individual users may define 40 41We accomplish all of that with only the project settings described in #1 above. 42 43Default settings are defined in this module. Project settings can be defined in 44.vscode/pw_project_settings.json and should be checked into the repository. User 45settings can be defined in .vscode/pw_user_settings.json and should not be 46checked into the repository. None of these settings have any effect until they 47are merged into VSC's settings (.vscode/settings.json) via the functions in this 48module. Those resulting settings are system-specific and should also not be 49checked into the repository. 50 51We provide the same structure to both tasks and extensions as well. Defaults 52are provided by Pigweed, can be augmented or overridden at the project level 53with .vscode/pw_project_tasks.json and .vscode/pw_project_extensions.json, 54can be augmented or overridden by an individual developer with 55.vscode/pw_user_tasks.json and .vscode/pw_user.extensions.json, and none of 56this takes effect until they are merged into VSC's active settings files 57(.vscode/tasks.json and .vscode/extensions.json) by running the appropriate 58command. 59""" 60 61from __future__ import annotations 62 63# TODO(chadnorvell): Import collections.OrderedDict when we don't need to 64# support Python 3.8 anymore. 65from enum import Enum 66from pathlib import Path 67import shutil 68import subprocess 69from typing import Any, Callable, OrderedDict 70 71from pw_cli.env import pigweed_environment 72 73from pw_ide.cpp import ClangdSettings, CppIdeFeaturesState 74 75from pw_ide.editors import ( 76 EditorSettingsDict, 77 EditorSettingsManager, 78 EditorSettingsTypesWithDefaults, 79 Json5FileFormat, 80) 81 82from pw_ide.python import PythonPaths 83from pw_ide.settings import PigweedIdeSettings 84 85env = pigweed_environment() 86 87 88def _local_clangd_settings(ide_settings: PigweedIdeSettings) -> dict[str, Any]: 89 """VSC settings for running clangd with Pigweed paths.""" 90 91 try: 92 clangd_settings = ClangdSettings(ide_settings) 93 94 return { 95 'clangd.path': str(clangd_settings.clangd_path), 96 'clangd.arguments': clangd_settings.arguments, 97 } 98 except FileNotFoundError: 99 return {} 100 101 102def _local_python_settings() -> dict[str, Any]: 103 """VSC settings for finding the Python virtualenv.""" 104 paths = PythonPaths() 105 return { 106 'python.defaultInterpreterPath': str(paths.interpreter), 107 } 108 109 110# The order is preserved despite starting with a plain dict because in Python 111# 3.6+, plain dicts are actually ordered as an implementation detail. This could 112# break on interpreters other than CPython, or if the implementation changes in 113# the future. However, for now, this is much more readable than the more robust 114# alternative of instantiating with a list of tuples. 115_DEFAULT_SETTINGS: EditorSettingsDict = OrderedDict( 116 { 117 "cmake.format.allowOptionalArgumentIndentation": True, 118 "editor.detectIndentation": False, 119 "editor.rulers": [80], 120 "editor.tabSize": 2, 121 "files.associations": OrderedDict({"*.inc": "cpp"}), 122 "files.exclude": OrderedDict( 123 { 124 "**/*.egg-info": True, 125 "**/.mypy_cache": True, 126 "**/__pycache__": True, 127 ".cache": True, 128 ".cipd": True, 129 ".environment": True, 130 ".presubmit": True, 131 ".pw_ide": True, 132 ".pw_ide.user.yaml": True, 133 "bazel-bin": True, 134 "bazel-out": True, 135 "bazel-pigweed": True, 136 "bazel-testlogs": True, 137 "environment": True, 138 "node_modules": True, 139 "out": True, 140 } 141 ), 142 "files.insertFinalNewline": True, 143 "files.trimTrailingWhitespace": True, 144 "search.useGlobalIgnoreFiles": True, 145 "npm.autoDetect": "off", 146 "C_Cpp.intelliSenseEngine": "disabled", 147 "[cpp]": OrderedDict( 148 {"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"} 149 ), 150 "python.analysis.diagnosticSeverityOverrides": OrderedDict( 151 # Due to our project structure, the linter spuriously thinks we're 152 # shadowing system modules any time we import them. This disables 153 # that check. 154 {"reportShadowedImports": "none"} 155 ), 156 # The "strict" mode is much more strict than what we currently enforce. 157 "python.analysis.typeCheckingMode": "basic", 158 # Restrict the search for Python files to the locations we expect to 159 # have Python files. This minimizes the time & RAM the LSP takes to 160 # parse the project. 161 "python.analysis.include": ["pw_*/py/**/*"], 162 "python.terminal.activateEnvironment": False, 163 "python.testing.unittestEnabled": True, 164 "[python]": OrderedDict({"editor.tabSize": 4}), 165 "typescript.tsc.autoDetect": "off", 166 "[gn]": OrderedDict({"editor.defaultFormatter": "msedge-dev.gnls"}), 167 "[proto3]": OrderedDict( 168 {"editor.defaultFormatter": "zxh404.vscode-proto3"} 169 ), 170 "[restructuredtext]": OrderedDict({"editor.tabSize": 3}), 171 } 172) 173 174_DEFAULT_TASKS: EditorSettingsDict = OrderedDict( 175 { 176 "version": "2.0.0", 177 "tasks": [ 178 { 179 "type": "process", 180 "label": "Pigweed: Format", 181 "command": "${config:python.defaultInterpreterPath}", 182 "args": [ 183 "-m", 184 "pw_ide.activate", 185 "-x 'pw format --fix'", 186 ], 187 "presentation": { 188 "focus": True, 189 }, 190 "problemMatcher": [], 191 }, 192 { 193 "type": "process", 194 "label": "Pigweed: Presubmit", 195 "command": "${config:python.defaultInterpreterPath}", 196 "args": [ 197 "-m", 198 "pw_ide.activate", 199 "-x 'pw presubmit'", 200 ], 201 "presentation": { 202 "focus": True, 203 }, 204 "problemMatcher": [], 205 }, 206 { 207 "label": "Pigweed: Set Python Virtual Environment", 208 "command": "${command:python.setInterpreter}", 209 "problemMatcher": [], 210 }, 211 { 212 "label": "Pigweed: Restart Python Language Server", 213 "command": "${command:python.analysis.restartLanguageServer}", 214 "problemMatcher": [], 215 }, 216 { 217 "label": "Pigweed: Restart C++ Language Server", 218 "command": "${command:clangd.restart}", 219 "problemMatcher": [], 220 }, 221 { 222 "type": "process", 223 "label": "Pigweed: Sync IDE", 224 "command": "${config:python.defaultInterpreterPath}", 225 "args": [ 226 "-m", 227 "pw_ide.activate", 228 "-x 'pw ide sync'", 229 ], 230 "presentation": { 231 "focus": True, 232 }, 233 "problemMatcher": [], 234 }, 235 { 236 "type": "process", 237 "label": "Pigweed: Current C++ Target Toolchain", 238 "command": "${config:python.defaultInterpreterPath}", 239 "args": [ 240 "-m", 241 "pw_ide.activate", 242 "-x 'pw ide cpp'", 243 ], 244 "presentation": { 245 "focus": True, 246 }, 247 "problemMatcher": [], 248 }, 249 { 250 "type": "process", 251 "label": "Pigweed: List C++ Target Toolchains", 252 "command": "${config:python.defaultInterpreterPath}", 253 "args": [ 254 "-m", 255 "pw_ide.activate", 256 "-x 'pw ide cpp --list'", 257 ], 258 "presentation": { 259 "focus": True, 260 }, 261 "problemMatcher": [], 262 }, 263 { 264 "type": "process", 265 "label": ( 266 "Pigweed: Change C++ Target Toolchain " 267 "without LSP restart" 268 ), 269 "command": "${config:python.defaultInterpreterPath}", 270 "args": [ 271 "-m", 272 "pw_ide.activate", 273 "-x 'pw ide cpp --set ${input:availableTargetToolchains}'", 274 ], 275 "presentation": { 276 "focus": True, 277 }, 278 "problemMatcher": [], 279 }, 280 { 281 "label": "Pigweed: Set C++ Target Toolchain", 282 "dependsOrder": "sequence", 283 "dependsOn": [ 284 "Pigweed: Change C++ Target Toolchain without LSP restart", 285 "Pigweed: Restart C++ Language Server", 286 ], 287 "presentation": { 288 "focus": True, 289 }, 290 "problemMatcher": [], 291 }, 292 ], 293 } 294) 295 296_DEFAULT_EXTENSIONS: EditorSettingsDict = OrderedDict( 297 { 298 "recommendations": [ 299 "llvm-vs-code-extensions.vscode-clangd", 300 "ms-python.mypy-type-checker", 301 "ms-python.python", 302 "ms-python.pylint", 303 "npclaudiu.vscode-gn", 304 "msedge-dev.gnls", 305 "zxh404.vscode-proto3", 306 "josetr.cmake-language-support-vscode", 307 ], 308 "unwantedRecommendations": [ 309 "ms-vscode.cpptools", 310 "persidskiy.vscode-gnformat", 311 "lextudio.restructuredtext", 312 ], 313 } 314) 315 316_DEFAULT_LAUNCH: EditorSettingsDict = OrderedDict( 317 { 318 "version": "0.2.0", 319 "configurations": [], 320 } 321) 322 323 324def _default_settings( 325 pw_ide_settings: PigweedIdeSettings, 326) -> EditorSettingsDict: 327 return OrderedDict( 328 { 329 **_DEFAULT_SETTINGS, 330 **_local_clangd_settings(pw_ide_settings), 331 **_local_python_settings(), 332 } 333 ) 334 335 336def _default_settings_no_side_effects( 337 _pw_ide_settings: PigweedIdeSettings, 338) -> EditorSettingsDict: 339 return OrderedDict( 340 { 341 **_DEFAULT_SETTINGS, 342 } 343 ) 344 345 346def _default_tasks( 347 pw_ide_settings: PigweedIdeSettings, 348 state: CppIdeFeaturesState | None = None, 349) -> EditorSettingsDict: 350 if state is None: 351 state = CppIdeFeaturesState(pw_ide_settings) 352 353 inputs = [ 354 { 355 "type": "pickString", 356 "id": "availableTargetToolchains", 357 "description": "Available target toolchains", 358 "options": sorted(list(state.targets)), 359 } 360 ] 361 362 return OrderedDict(**_DEFAULT_TASKS, inputs=inputs) 363 364 365def _default_extensions( 366 _pw_ide_settings: PigweedIdeSettings, 367) -> EditorSettingsDict: 368 return _DEFAULT_EXTENSIONS 369 370 371def _default_launch( 372 _pw_ide_settings: PigweedIdeSettings, 373) -> EditorSettingsDict: 374 return _DEFAULT_LAUNCH 375 376 377DEFAULT_SETTINGS_PATH: Callable[[PigweedIdeSettings], Path] = ( 378 lambda settings: settings.workspace_root / '.vscode' 379) 380 381 382class VscSettingsType(Enum): 383 """Visual Studio Code settings files. 384 385 VSC supports editor settings (``settings.json``), recommended 386 extensions (``extensions.json``), tasks (``tasks.json``), and 387 launch/debug configurations (``launch.json``). 388 """ 389 390 SETTINGS = 'settings' 391 TASKS = 'tasks' 392 EXTENSIONS = 'extensions' 393 LAUNCH = 'launch' 394 395 @classmethod 396 def all(cls) -> list[VscSettingsType]: 397 return list(cls) 398 399 400class VscSettingsManager(EditorSettingsManager[VscSettingsType]): 401 """Manages all settings for Visual Studio Code.""" 402 403 default_settings_dir = DEFAULT_SETTINGS_PATH 404 file_format = Json5FileFormat() 405 406 types_with_defaults: EditorSettingsTypesWithDefaults = { 407 VscSettingsType.SETTINGS: _default_settings, 408 VscSettingsType.TASKS: _default_tasks, 409 VscSettingsType.EXTENSIONS: _default_extensions, 410 VscSettingsType.LAUNCH: _default_launch, 411 } 412 413 414class VscSettingsManagerNoSideEffects(EditorSettingsManager[VscSettingsType]): 415 """This is like VscSettingsManager, but optimized for unit testing.""" 416 417 default_settings_dir = DEFAULT_SETTINGS_PATH 418 file_format = Json5FileFormat() 419 420 types_with_defaults: EditorSettingsTypesWithDefaults = { 421 VscSettingsType.SETTINGS: _default_settings_no_side_effects, 422 VscSettingsType.TASKS: _default_tasks, 423 VscSettingsType.EXTENSIONS: _default_extensions, 424 VscSettingsType.LAUNCH: _default_launch, 425 } 426 427 428def build_extension(pw_root: Path): 429 """Build the VS Code extension.""" 430 431 license_path = pw_root / 'LICENSE' 432 icon_path = pw_root.parent / 'icon.png' 433 434 vsc_ext_path = pw_root / 'pw_ide' / 'ts' / 'pigweed-vscode' 435 out_path = vsc_ext_path / 'out' 436 dist_path = vsc_ext_path / 'dist' 437 temp_license_path = vsc_ext_path / 'LICENSE' 438 temp_icon_path = vsc_ext_path / 'icon.png' 439 440 shutil.rmtree(out_path, ignore_errors=True) 441 shutil.rmtree(dist_path, ignore_errors=True) 442 shutil.copy(license_path, temp_license_path) 443 shutil.copy(icon_path, temp_icon_path) 444 445 try: 446 subprocess.run(['npm', 'install'], check=True, cwd=vsc_ext_path) 447 subprocess.run(['npm', 'run', 'compile'], check=True, cwd=vsc_ext_path) 448 subprocess.run(['vsce', 'package'], check=True, cwd=vsc_ext_path) 449 except subprocess.CalledProcessError as e: 450 raise e 451 finally: 452 temp_license_path.unlink() 453 temp_icon_path.unlink() 454