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 66import os 67from pathlib import Path 68import shutil 69import subprocess 70from typing import Any, OrderedDict 71 72from pw_cli.env import pigweed_environment 73 74from pw_ide.cpp import ClangdSettings, CppIdeFeaturesState 75 76from pw_ide.editors import ( 77 EditorSettingsDict, 78 EditorSettingsManager, 79 EditorSettingsTypesWithDefaults, 80 Json5FileFormat, 81) 82 83from pw_ide.python import PythonPaths 84from pw_ide.settings import PigweedIdeSettings 85 86env = pigweed_environment() 87 88 89def _local_clangd_settings(ide_settings: PigweedIdeSettings) -> dict[str, Any]: 90 """VSC settings for running clangd with Pigweed paths.""" 91 clangd_settings = ClangdSettings(ide_settings) 92 return { 93 'clangd.path': str(clangd_settings.clangd_path), 94 'clangd.arguments': clangd_settings.arguments, 95 } 96 97 98def _local_python_settings() -> dict[str, Any]: 99 """VSC settings for finding the Python virtualenv.""" 100 paths = PythonPaths() 101 return { 102 'python.defaultInterpreterPath': str(paths.interpreter), 103 } 104 105 106# The order is preserved despite starting with a plain dict because in Python 107# 3.6+, plain dicts are actually ordered as an implementation detail. This could 108# break on interpreters other than CPython, or if the implementation changes in 109# the future. However, for now, this is much more readable than the more robust 110# alternative of instantiating with a list of tuples. 111_DEFAULT_SETTINGS: EditorSettingsDict = OrderedDict( 112 { 113 "cmake.format.allowOptionalArgumentIndentation": True, 114 "editor.detectIndentation": False, 115 "editor.rulers": [80], 116 "editor.tabSize": 2, 117 "files.associations": OrderedDict({"*.inc": "cpp"}), 118 "files.exclude": OrderedDict( 119 { 120 "**/*.egg-info": True, 121 "**/.mypy_cache": True, 122 "**/__pycache__": True, 123 ".cache": True, 124 ".cipd": True, 125 ".environment": True, 126 ".presubmit": True, 127 ".pw_ide": True, 128 ".pw_ide.user.yaml": True, 129 "bazel-bin": True, 130 "bazel-out": True, 131 "bazel-pigweed": True, 132 "bazel-testlogs": True, 133 "environment": True, 134 "node_modules": True, 135 "out": True, 136 } 137 ), 138 "files.insertFinalNewline": True, 139 "files.trimTrailingWhitespace": True, 140 "search.useGlobalIgnoreFiles": True, 141 "grunt.autoDetect": "off", 142 "gulp.autoDetect": "off", 143 "jake.autoDetect": "off", 144 "npm.autoDetect": "off", 145 "C_Cpp.intelliSenseEngine": "disabled", 146 "[cpp]": OrderedDict( 147 {"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"} 148 ), 149 "python.analysis.diagnosticSeverityOverrides": OrderedDict( 150 # Due to our project structure, the linter spuriously thinks we're 151 # shadowing system modules any time we import them. This disables 152 # that check. 153 {"reportShadowedImports": "none"} 154 ), 155 # The "strict" mode is much more strict than what we currently enforce. 156 "python.analysis.typeCheckingMode": "basic", 157 # Restrict the search for Python files to the locations we expect to 158 # have Python files. This minimizes the time & RAM the LSP takes to 159 # parse the project. 160 "python.analysis.include": ["pw_*/py/**/*"], 161 "python.terminal.activateEnvironment": False, 162 "python.testing.unittestEnabled": True, 163 "[python]": OrderedDict({"editor.tabSize": 4}), 164 "typescript.tsc.autoDetect": "off", 165 "[gn]": OrderedDict({"editor.defaultFormatter": "msedge-dev.gnls"}), 166 "[proto3]": OrderedDict( 167 {"editor.defaultFormatter": "zxh404.vscode-proto3"} 168 ), 169 "[restructuredtext]": OrderedDict({"editor.tabSize": 3}), 170 } 171) 172 173_DEFAULT_TASKS: EditorSettingsDict = OrderedDict( 174 { 175 "version": "2.0.0", 176 "tasks": [ 177 { 178 "type": "process", 179 "label": "Pigweed: Format", 180 "command": "${config:python.defaultInterpreterPath}", 181 "args": [ 182 "-m", 183 "pw_ide.activate", 184 "-x 'pw format --fix'", 185 ], 186 "presentation": { 187 "focus": True, 188 }, 189 "problemMatcher": [], 190 }, 191 { 192 "type": "process", 193 "label": "Pigweed: Presubmit", 194 "command": "${config:python.defaultInterpreterPath}", 195 "args": [ 196 "-m", 197 "pw_ide.activate", 198 "-x 'pw presubmit'", 199 ], 200 "presentation": { 201 "focus": True, 202 }, 203 "problemMatcher": [], 204 }, 205 { 206 "label": "Pigweed: Set Python Virtual Environment", 207 "command": "${command:python.setInterpreter}", 208 "problemMatcher": [], 209 }, 210 { 211 "label": "Pigweed: Restart Python Language Server", 212 "command": "${command:python.analysis.restartLanguageServer}", 213 "problemMatcher": [], 214 }, 215 { 216 "label": "Pigweed: Restart C++ Language Server", 217 "command": "${command:clangd.restart}", 218 "problemMatcher": [], 219 }, 220 { 221 "type": "process", 222 "label": "Pigweed: Sync IDE", 223 "command": "${config:python.defaultInterpreterPath}", 224 "args": [ 225 "-m", 226 "pw_ide.activate", 227 "-x 'pw ide sync'", 228 ], 229 "presentation": { 230 "focus": True, 231 }, 232 "problemMatcher": [], 233 }, 234 { 235 "type": "process", 236 "label": "Pigweed: Current C++ Target Toolchain", 237 "command": "${config:python.defaultInterpreterPath}", 238 "args": [ 239 "-m", 240 "pw_ide.activate", 241 "-x 'pw ide cpp'", 242 ], 243 "presentation": { 244 "focus": True, 245 }, 246 "problemMatcher": [], 247 }, 248 { 249 "type": "process", 250 "label": "Pigweed: List C++ Target Toolchains", 251 "command": "${config:python.defaultInterpreterPath}", 252 "args": [ 253 "-m", 254 "pw_ide.activate", 255 "-x 'pw ide cpp --list'", 256 ], 257 "presentation": { 258 "focus": True, 259 }, 260 "problemMatcher": [], 261 }, 262 { 263 "type": "process", 264 "label": ( 265 "Pigweed: Change C++ Target Toolchain " 266 "without LSP restart" 267 ), 268 "command": "${config:python.defaultInterpreterPath}", 269 "args": [ 270 "-m", 271 "pw_ide.activate", 272 "-x 'pw ide cpp --set ${input:availableTargetToolchains}'", 273 ], 274 "presentation": { 275 "focus": True, 276 }, 277 "problemMatcher": [], 278 }, 279 { 280 "label": "Pigweed: Set C++ Target Toolchain", 281 "dependsOrder": "sequence", 282 "dependsOn": [ 283 "Pigweed: Change C++ Target Toolchain without LSP restart", 284 "Pigweed: Restart C++ Language Server", 285 ], 286 "presentation": { 287 "focus": True, 288 }, 289 "problemMatcher": [], 290 }, 291 ], 292 } 293) 294 295_DEFAULT_EXTENSIONS: EditorSettingsDict = OrderedDict( 296 { 297 "recommendations": [ 298 "llvm-vs-code-extensions.vscode-clangd", 299 "ms-python.mypy-type-checker", 300 "ms-python.python", 301 "ms-python.pylint", 302 "npclaudiu.vscode-gn", 303 "msedge-dev.gnls", 304 "zxh404.vscode-proto3", 305 "josetr.cmake-language-support-vscode", 306 ], 307 "unwantedRecommendations": [ 308 "ms-vscode.cpptools", 309 "persidskiy.vscode-gnformat", 310 "lextudio.restructuredtext", 311 ], 312 } 313) 314 315_DEFAULT_LAUNCH: EditorSettingsDict = OrderedDict( 316 { 317 "version": "0.2.0", 318 "configurations": [], 319 } 320) 321 322 323def _default_settings( 324 pw_ide_settings: PigweedIdeSettings, 325) -> EditorSettingsDict: 326 return OrderedDict( 327 { 328 **_DEFAULT_SETTINGS, 329 **_local_clangd_settings(pw_ide_settings), 330 **_local_python_settings(), 331 } 332 ) 333 334 335def _default_tasks( 336 pw_ide_settings: PigweedIdeSettings, 337 state: CppIdeFeaturesState | None = None, 338) -> EditorSettingsDict: 339 if state is None: 340 state = CppIdeFeaturesState(pw_ide_settings) 341 342 inputs = [ 343 { 344 "type": "pickString", 345 "id": "availableTargetToolchains", 346 "description": "Available target toolchains", 347 "options": sorted(list(state.targets)), 348 } 349 ] 350 351 return OrderedDict(**_DEFAULT_TASKS, inputs=inputs) 352 353 354def _default_extensions( 355 _pw_ide_settings: PigweedIdeSettings, 356) -> EditorSettingsDict: 357 return _DEFAULT_EXTENSIONS 358 359 360def _default_launch( 361 _pw_ide_settings: PigweedIdeSettings, 362) -> EditorSettingsDict: 363 return _DEFAULT_LAUNCH 364 365 366DEFAULT_SETTINGS_PATH = Path(os.path.expandvars('$PW_PROJECT_ROOT')) / '.vscode' 367 368 369class VscSettingsType(Enum): 370 """Visual Studio Code settings files. 371 372 VSC supports editor settings (``settings.json``), recommended 373 extensions (``extensions.json``), tasks (``tasks.json``), and 374 launch/debug configurations (``launch.json``). 375 """ 376 377 SETTINGS = 'settings' 378 TASKS = 'tasks' 379 EXTENSIONS = 'extensions' 380 LAUNCH = 'launch' 381 382 @classmethod 383 def all(cls) -> list[VscSettingsType]: 384 return list(cls) 385 386 387class VscSettingsManager(EditorSettingsManager[VscSettingsType]): 388 """Manages all settings for Visual Studio Code.""" 389 390 default_settings_dir = DEFAULT_SETTINGS_PATH 391 file_format = Json5FileFormat() 392 393 types_with_defaults: EditorSettingsTypesWithDefaults = { 394 VscSettingsType.SETTINGS: _default_settings, 395 VscSettingsType.TASKS: _default_tasks, 396 VscSettingsType.EXTENSIONS: _default_extensions, 397 VscSettingsType.LAUNCH: _default_launch, 398 } 399 400 401def build_extension(pw_root: Path): 402 """Build the VS Code extension.""" 403 404 license_path = pw_root / 'LICENSE' 405 icon_path = pw_root.parent / 'icon.png' 406 407 vsc_ext_path = pw_root / 'pw_ide' / 'ts' / 'pigweed-vscode' 408 out_path = vsc_ext_path / 'out' 409 dist_path = vsc_ext_path / 'dist' 410 temp_license_path = vsc_ext_path / 'LICENSE' 411 temp_icon_path = vsc_ext_path / 'icon.png' 412 413 shutil.rmtree(out_path, ignore_errors=True) 414 shutil.rmtree(dist_path, ignore_errors=True) 415 shutil.copy(license_path, temp_license_path) 416 shutil.copy(icon_path, temp_icon_path) 417 418 try: 419 subprocess.run(['npm', 'install'], check=True, cwd=vsc_ext_path) 420 subprocess.run(['npm', 'run', 'compile'], check=True, cwd=vsc_ext_path) 421 subprocess.run(['vsce', 'package'], check=True, cwd=vsc_ext_path) 422 except subprocess.CalledProcessError as e: 423 raise e 424 finally: 425 temp_license_path.unlink() 426 temp_icon_path.unlink() 427