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 61# TODO(chadnorvell): Import collections.OrderedDict when we don't need to 62# support Python 3.8 anymore. 63from enum import Enum 64import json 65import os 66from pathlib import Path 67import platform 68from typing import Any, Dict, List, OrderedDict 69 70from pw_ide.activate import BashShellModifier 71from pw_ide.cpp import ClangdSettings 72 73from pw_ide.editors import ( 74 EditorSettingsDict, 75 EditorSettingsManager, 76 EditorSettingsTypesWithDefaults, 77 Json5FileFormat, 78) 79 80from pw_ide.python import PythonPaths 81from pw_ide.settings import PigweedIdeSettings 82 83 84def _vsc_os(system: str = platform.system()): 85 """Return the OS tag that VSC expects.""" 86 if system == 'Darwin': 87 return 'osx' 88 89 return system.lower() 90 91 92def _activated_env() -> OrderedDict[str, Any]: 93 """Return the environment diff needed to provide Pigweed activation. 94 95 The integrated terminal will already include the user's default environment 96 (e.g. from their shell init scripts). This provides the modifications to 97 the environment needed for Pigweed activation. 98 """ 99 # Not all environments have an actions.json, which this ultimately relies 100 # on (e.g. tests in CI). No problem, just return an empty dict instead. 101 try: 102 env = ( 103 BashShellModifier(env_only=True, path_var='${env:PATH}') 104 .modify_env() 105 .env_mod 106 ) 107 except (FileNotFoundError, json.JSONDecodeError): 108 env = dict() 109 110 return OrderedDict(env) 111 112 113def _local_terminal_integrated_env() -> Dict[str, Any]: 114 """VSC setting to activate the integrated terminal.""" 115 return {f'terminal.integrated.env.{_vsc_os()}': _activated_env()} 116 117 118def _local_clangd_settings(ide_settings: PigweedIdeSettings) -> Dict[str, Any]: 119 """VSC settings for running clangd with Pigweed paths.""" 120 clangd_settings = ClangdSettings(ide_settings) 121 return { 122 'clangd.path': str(clangd_settings.clangd_path), 123 'clangd.arguments': clangd_settings.arguments, 124 } 125 126 127def _local_python_settings() -> Dict[str, Any]: 128 """VSC settings for finding the Python virtualenv.""" 129 paths = PythonPaths() 130 return { 131 'python.defaultInterpreterPath': str(paths.interpreter), 132 'python.formatting.yapfPath': str(paths.bin_dir / 'yapf'), 133 } 134 135 136# The order is preserved despite starting with a plain dict because in Python 137# 3.6+, plain dicts are actually ordered as an implementation detail. This could 138# break on interpreters other than CPython, or if the implementation changes in 139# the future. However, for now, this is much more readable than the more robust 140# alternative of instantiating with a list of tuples. 141_DEFAULT_SETTINGS: EditorSettingsDict = OrderedDict( 142 { 143 "editor.detectIndentation": False, 144 "editor.rulers": [80], 145 "editor.tabSize": 2, 146 "files.associations": OrderedDict({"*.inc": "cpp"}), 147 "files.exclude": OrderedDict( 148 { 149 "**/*.egg-info": True, 150 "**/.mypy_cache": True, 151 "**/__pycache__": True, 152 ".cache": True, 153 ".cipd": True, 154 ".environment": True, 155 ".presubmit": True, 156 ".pw_ide": True, 157 ".pw_ide.user.yaml": True, 158 "bazel-bin": True, 159 "bazel-out": True, 160 "bazel-pigweed": True, 161 "bazel-testlogs": True, 162 "build": True, 163 "environment": True, 164 "node_modules": True, 165 "out": True, 166 } 167 ), 168 "files.insertFinalNewline": True, 169 "files.trimTrailingWhitespace": True, 170 "search.useGlobalIgnoreFiles": True, 171 "grunt.autoDetect": "off", 172 "gulp.autoDetect": "off", 173 "jake.autoDetect": "off", 174 "npm.autoDetect": "off", 175 "clangd.onConfigChanged": "restart", 176 "C_Cpp.intelliSenseEngine": "Disabled", 177 "[cpp]": OrderedDict( 178 {"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"} 179 ), 180 "python.formatting.provider": "yapf", 181 "[python]": OrderedDict({"editor.tabSize": 4}), 182 "typescript.tsc.autoDetect": "off", 183 "[gn]": OrderedDict({"editor.defaultFormatter": "msedge-dev.gnls"}), 184 "[proto3]": OrderedDict( 185 {"editor.defaultFormatter": "zxh404.vscode-proto3"} 186 ), 187 } 188) 189 190# pylint: disable=line-too-long 191_DEFAULT_TASKS: EditorSettingsDict = OrderedDict( 192 { 193 "version": "2.0.0", 194 "tasks": [ 195 { 196 "type": "shell", 197 "label": "Pigweed IDE: Format", 198 "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw format --fix'", 199 "problemMatcher": [], 200 }, 201 { 202 "type": "shell", 203 "label": "Pigweed IDE: Presubmit", 204 "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw presubmit'", 205 "problemMatcher": [], 206 }, 207 { 208 "label": "Pigweed IDE: Set Python Virtual Environment", 209 "command": "${command:python.setInterpreter}", 210 "problemMatcher": [], 211 }, 212 { 213 "label": "Pigweed IDE: Restart Python Language Server", 214 "command": "${command:python.analysis.restartLanguageServer}", 215 "problemMatcher": [], 216 }, 217 { 218 "label": "Pigweed IDE: Restart C++ Language Server", 219 "command": "${command:clangd.restart}", 220 "problemMatcher": [], 221 }, 222 { 223 "type": "shell", 224 "label": "Pigweed IDE: Process C++ Compilation Database from GN", 225 "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --gn --process out/compile_commands.json'", 226 "problemMatcher": [], 227 }, 228 { 229 "type": "shell", 230 "label": "Pigweed IDE: Setup", 231 "command": "python3 ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide setup'", 232 "problemMatcher": [], 233 }, 234 { 235 "type": "shell", 236 "label": "Pigweed IDE: Current C++ Code Analysis Target", 237 "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp'", 238 "problemMatcher": [], 239 }, 240 { 241 "type": "shell", 242 "label": "Pigweed IDE: List C++ Code Analysis Targets", 243 "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --list'", 244 "problemMatcher": [], 245 }, 246 { 247 "type": "shell", 248 "label": "Pigweed IDE: Set C++ Code Analysis Target", 249 "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --set ${input:target}'", 250 "problemMatcher": [], 251 }, 252 ], 253 "inputs": [ 254 { 255 "id": "target", 256 "type": "promptString", 257 "description": "C++ code analysis target", 258 } 259 ], 260 } 261) 262# pylint: enable=line-too-long 263 264_DEFAULT_EXTENSIONS: EditorSettingsDict = OrderedDict( 265 { 266 "recommendations": [ 267 "llvm-vs-code-extensions.vscode-clangd", 268 "ms-python.python", 269 "npclaudiu.vscode-gn", 270 "msedge-dev.gnls", 271 "zxh404.vscode-proto3", 272 "josetr.cmake-language-support-vscode", 273 "swyddfa.esbonio", 274 ], 275 "unwantedRecommendations": [ 276 "ms-vscode.cpptools", 277 "persidskiy.vscode-gnformat", 278 "lextudio.restructuredtext", 279 "trond-snekvik.simple-rst", 280 ], 281 } 282) 283 284 285def _default_settings( 286 pw_ide_settings: PigweedIdeSettings, 287) -> EditorSettingsDict: 288 return OrderedDict( 289 { 290 **_DEFAULT_SETTINGS, 291 **_local_terminal_integrated_env(), 292 **_local_clangd_settings(pw_ide_settings), 293 **_local_python_settings(), 294 } 295 ) 296 297 298def _default_tasks(_pw_ide_settings: PigweedIdeSettings) -> EditorSettingsDict: 299 return _DEFAULT_TASKS 300 301 302def _default_extensions( 303 _pw_ide_settings: PigweedIdeSettings, 304) -> EditorSettingsDict: 305 return _DEFAULT_EXTENSIONS 306 307 308DEFAULT_SETTINGS_PATH = Path(os.path.expandvars('$PW_PROJECT_ROOT')) / '.vscode' 309 310 311class VscSettingsType(Enum): 312 """Visual Studio Code settings files. 313 314 VSC supports editor settings (``settings.json``), recommended 315 extensions (``extensions.json``), and tasks (``tasks.json``). 316 """ 317 318 SETTINGS = 'settings' 319 TASKS = 'tasks' 320 EXTENSIONS = 'extensions' 321 322 @classmethod 323 def all(cls) -> List['VscSettingsType']: 324 return list(cls) 325 326 327class VscSettingsManager(EditorSettingsManager[VscSettingsType]): 328 """Manages all settings for Visual Studio Code.""" 329 330 default_settings_dir = DEFAULT_SETTINGS_PATH 331 file_format = Json5FileFormat() 332 333 types_with_defaults: EditorSettingsTypesWithDefaults = { 334 VscSettingsType.SETTINGS: _default_settings, 335 VscSettingsType.TASKS: _default_tasks, 336 VscSettingsType.EXTENSIONS: _default_extensions, 337 } 338