• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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