#!/usr/bin/env bash # Install this project's local VS Code extensions into the container's VS Code server. # # devcontainer.json has no place for keyboard shortcuts or editor behaviour beyond # settings: customizations.vscode only understands "settings" and "extensions", and # keybindings.json is resolved by the VS Code client, not by the server -- so # dropping a file into ~/.vscode-server/data/User/ does nothing. What *does* travel # with the container is an extension. So each directory listed below is packaged as # a .vsix here and installed; edit those directories to change behaviour, this # script only ships them. set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" extensions=( overleaf-keymap # Overleaf's keyboard shortcuts paste-figure # Ctrl+V an image -> saved to img/ and wrapped in a figure ) # The Dev Containers extension installs customizations.vscode.extensions around # the same time postCreateCommand fires, and both writes touch extensions.json. # Wait (bounded) for its marker so we don't interleave with it. marker="$HOME/.vscode-server/data/Machine/.installExtensionsMarker" for _ in $(seq 1 60); do [ -e "$marker" ] && break sleep 1 done # The remote CLI isn't on PATH during postCreateCommand; find the server binary. code_server="$(ls -1t "$HOME"/.vscode-server/bin/*/bin/code-server 2>/dev/null | head -n1 || true)" if [ -z "$code_server" ]; then echo "install-extensions: no VS Code server found, skipping local extensions." >&2 exit 0 fi build="$(mktemp -d)" trap 'rm -rf "$build"' EXIT for name in "${extensions[@]}"; do src="${here}/${name}" vsix="${build}/${name}.vsix" python3 - "$src" "$vsix" <<'PY' import json, mimetypes, os, sys, zipfile from xml.sax.saxutils import escape src, out = sys.argv[1], sys.argv[2] pkg = json.load(open(f"{src}/package.json")) ident, version, publisher = pkg["name"], pkg["version"], pkg["publisher"] kinds = ",".join(pkg.get("extensionKind", ["workspace"])) manifest = f""" {escape(pkg["displayName"])} {escape(pkg["description"])} {escape(",".join(pkg.get("categories", [])))} {escape(",".join(pkg.get("categories", [])))} Public """ # Every file of the extension goes in, so extensions can ship code, not just a manifest. files = [] for root, _, names in os.walk(src): for name in sorted(names): path = os.path.join(root, name) files.append((path, os.path.relpath(path, src).replace(os.sep, "/"))) defaults = {"json": "application/json", "js": "application/javascript", "vsixmanifest": "text/xml", "xml": "text/xml"} for path, _ in files: ext = os.path.splitext(path)[1].lstrip(".").lower() if ext and ext not in defaults: defaults[ext] = mimetypes.guess_type(path)[0] or "application/octet-stream" content_types = "\n".join( ['', '', *[f' ' for ext, ctype in sorted(defaults.items())], '', '']) with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: z.writestr("extension.vsixmanifest", manifest) z.writestr("[Content_Types].xml", content_types) for path, rel in files: z.write(path, f"extension/{rel}") PY "$code_server" --install-extension "$vsix" --force done