• 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
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