xref: /aosp_15_r20/external/pigweed/pw_ide/py/pw_ide/vscode.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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