diff --git a/.gitignore b/.gitignore
index 9b1a06b3c..50a05bb03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,8 @@ build/
cmake-build-*/
.clangd
CMakeUserPresets.json
+# machine-specific Conan cache paths, generated by scripts/gen-vscode-env.py
+.vscode/.env
offline/
## PDF CJK CMap input data (fetched by tools/pdf/generate_cid_data.py)
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..751ec8340
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,11 @@
+{
+ "recommendations": [
+ "ms-vscode.cmake-tools",
+ "llvm-vs-code-extensions.vscode-clangd",
+ "vadimcn.vscode-lldb",
+ "twxs.cmake"
+ ],
+ "unwantedRecommendations": [
+ "ms-vscode.cpptools"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..0cce01e4f
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,69 @@
+{
+ "version": "0.2.0",
+ "inputs": [
+ {
+ "id": "gtestFilter",
+ "type": "promptString",
+ "description": "GoogleTest --gtest_filter",
+ "default": "*"
+ },
+ {
+ "id": "translateInput",
+ "type": "promptString",
+ "description": "Input document to translate (absolute path)"
+ },
+ {
+ "id": "translateOutput",
+ "type": "promptString",
+ "description": "Output directory for HTML",
+ "default": "${workspaceFolder}/cmake-build-relwithdebinfo/output"
+ },
+ {
+ "id": "metaInput",
+ "type": "promptString",
+ "description": "Input document for meta"
+ }
+ ],
+ "configurations": [
+ {
+ "name": "Debug odr_test (filtered)",
+ "type": "lldb",
+ "request": "launch",
+ "program": "${workspaceFolder}/cmake-build-relwithdebinfo/test/odr_test",
+ "args": ["--gtest_filter=${input:gtestFilter}"],
+ "cwd": "${workspaceFolder}/cmake-build-relwithdebinfo",
+ "envFile": "${workspaceFolder}/.vscode/.env",
+ "preLaunchTask": "build: odr_test"
+ },
+ {
+ "name": "Debug translate