Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/tree-sitter/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"id": "tree-sitter",
"version": "1.0.0",
"name": "Tree-sitter",
"description": "Installs the Tree-sitter CLI for incremental parsing. Optionally compiles grammars (PHP, TypeScript, JavaScript, Go, Dart, Python, YAML) from official repos.",
"documentationURL": "https://tree-sitter.github.io/tree-sitter/",
"licenseURL": "https://github.com/tree-sitter/tree-sitter/blob/master/LICENSE",
"options": {
"version": {
"type": "string",
"default": "latest",
"description": "Tree-sitter CLI version: 'latest' or a specific version (e.g. '0.25.4')"
},
"grammarPhp": {
"type": "boolean",
"default": false,
"description": "Compile and install the PHP grammar (tree-sitter/tree-sitter-php)"
},
"grammarTypescript": {
"type": "boolean",
"default": false,
"description": "Compile and install the TypeScript grammar (tree-sitter/tree-sitter-typescript)"
},
"grammarJavascript": {
"type": "boolean",
"default": false,
"description": "Compile and install the JavaScript grammar (tree-sitter/tree-sitter-javascript)"
},
"grammarGo": {
"type": "boolean",
"default": false,
"description": "Compile and install the Go grammar (tree-sitter/tree-sitter-go)"
},
"grammarDart": {
"type": "boolean",
"default": false,
"description": "Compile and install the Dart grammar (UserNobody14/tree-sitter-dart)"
},
"grammarPython": {
"type": "boolean",
"default": false,
"description": "Compile and install the Python grammar (tree-sitter/tree-sitter-python)"
},
"grammarYaml": {
"type": "boolean",
"default": false,
"description": "Compile and install the YAML grammar (tree-sitter-grammars/tree-sitter-yaml)"
}
},
"containerEnv": {
"TREE_SITTER_DIR": "/usr/local/lib/tree-sitter"
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils",
"ghcr.io/devcontainers/features/git"
]
}
128 changes: 128 additions & 0 deletions src/tree-sitter/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/bin/bash
set -e

# --- Options injected by devcontainer feature engine ---
VERSION="${VERSION:-latest}"
GRAMMAR_PHP="${GRAMMARPHP:-false}"
GRAMMAR_TYPESCRIPT="${GRAMMARTYPESCRIPT:-false}"
GRAMMAR_JAVASCRIPT="${GRAMMARJAVASCRIPT:-false}"
GRAMMAR_GO="${GRAMMARGO:-false}"
GRAMMAR_DART="${GRAMMARDART:-false}"
GRAMMAR_PYTHON="${GRAMMARPYTHON:-false}"
GRAMMAR_YAML="${GRAMMARYAML:-false}"

TREE_SITTER_DIR="/usr/local/lib/tree-sitter"

# --- Helpers ---
check_packages() {
if ! dpkg -s "$@" >/dev/null 2>&1; then
if [ "$(find /var/lib/apt/lists/* 2>/dev/null | head -1)" = "" ]; then
echo "==> Running apt-get update..."
apt-get update -y
fi
apt-get install -y --no-install-recommends "$@"
fi
}

any_grammar_enabled() {
[ "$GRAMMAR_PHP" = "true" ] || [ "$GRAMMAR_TYPESCRIPT" = "true" ] || \
[ "$GRAMMAR_JAVASCRIPT" = "true" ] || [ "$GRAMMAR_GO" = "true" ] || \
[ "$GRAMMAR_DART" = "true" ] || [ "$GRAMMAR_PYTHON" = "true" ] || \
[ "$GRAMMAR_YAML" = "true" ]
}

# Clone a grammar repo, build the .so, clean up.
# Usage: build_grammar <github-org/repo> <output-name> [subdir]
build_grammar() {
local repo="$1"
local name="$2"
local subdir="${3:-}"
local clone_dir="/tmp/ts-${name}"

echo "==> Building grammar: ${name} (${repo})..."
git clone --depth 1 "https://github.com/${repo}.git" "$clone_dir"

local build_dir="$clone_dir"
if [ -n "$subdir" ]; then
build_dir="${clone_dir}/${subdir}"
fi

tree-sitter build --output "${TREE_SITTER_DIR}/${name}.so" "$build_dir"
rm -rf "$clone_dir"
echo "==> Grammar ${name} installed: ${TREE_SITTER_DIR}/${name}.so"
}

# --- Main ---
echo "==> Tree-sitter feature: version=${VERSION}"

# 1. Base dependencies
check_packages curl ca-certificates

# 2. Resolve version
if [ "$VERSION" = "latest" ]; then
# NOTE: Anonymous GitHub API rate limit is 60 req/hour.
# If builds fail here, pin a specific version instead.
VERSION=$(curl -fsSL "https://api.github.com/repos/tree-sitter/tree-sitter/releases/latest" \
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [ -z "$VERSION" ]; then
echo "ERROR: Failed to resolve latest tree-sitter version from GitHub API"
exit 1
fi
echo "==> Resolved latest version: ${VERSION}"
fi

# 3. Download pre-built CLI binary
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) TS_ARCH="x64" ;;
arm64) TS_ARCH="arm64" ;;
*)
echo "ERROR: Unsupported architecture: $ARCH"
exit 1
;;
esac

DOWNLOAD_URL="https://github.com/tree-sitter/tree-sitter/releases/download/v${VERSION}/tree-sitter-linux-${TS_ARCH}.gz"
echo "==> Downloading tree-sitter v${VERSION} for ${TS_ARCH}..."
curl -fsSL "$DOWNLOAD_URL" | gunzip > /usr/local/bin/tree-sitter
chmod +x /usr/local/bin/tree-sitter

echo "==> Tree-sitter $(tree-sitter --version) installed at $(command -v tree-sitter)"

# 4. Grammar compilation (only if at least one grammar is enabled)
if any_grammar_enabled; then
echo "==> Installing build dependencies for grammars..."
check_packages gcc libc6-dev git

mkdir -p "$TREE_SITTER_DIR"

if [ "$GRAMMAR_PHP" = "true" ]; then
build_grammar "tree-sitter/tree-sitter-php" "php" "php"
fi
if [ "$GRAMMAR_TYPESCRIPT" = "true" ]; then
build_grammar "tree-sitter/tree-sitter-typescript" "typescript" "typescript"
fi
if [ "$GRAMMAR_JAVASCRIPT" = "true" ]; then
build_grammar "tree-sitter/tree-sitter-javascript" "javascript"
fi
if [ "$GRAMMAR_GO" = "true" ]; then
build_grammar "tree-sitter/tree-sitter-go" "go"
fi
if [ "$GRAMMAR_DART" = "true" ]; then
build_grammar "UserNobody14/tree-sitter-dart" "dart"
fi
if [ "$GRAMMAR_PYTHON" = "true" ]; then
build_grammar "tree-sitter/tree-sitter-python" "python"
fi
if [ "$GRAMMAR_YAML" = "true" ]; then
build_grammar "tree-sitter-grammars/tree-sitter-yaml" "yaml"
fi

# Give remote user ownership so tree-sitter can write at runtime
REMOTE_USER="${_REMOTE_USER:-vscode}"
if id "$REMOTE_USER" &>/dev/null; then
chown -R "${REMOTE_USER}:${REMOTE_USER}" "$TREE_SITTER_DIR"
fi
fi

echo "==> Tree-sitter feature install complete"
6 changes: 6 additions & 0 deletions test-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NO_CACHE=false

# Parse flags
BASE_IMAGE="mcr.microsoft.com/devcontainers/base:ubuntu"
while [[ "$1" == -* ]]; do
case "$1" in
--no-cache) NO_CACHE=true; shift ;;
--base-image) BASE_IMAGE="$2"; shift 2 ;;
*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
esac
done
Expand Down Expand Up @@ -42,6 +44,10 @@ run_tests() {
--project-folder "$SCRIPT_DIR"
)

if [ -n "$BASE_IMAGE" ]; then
args+=(--base-image "$BASE_IMAGE")
fi

if [ -n "$SCENARIO" ]; then
args+=(--filter "$SCENARIO")
fi
Expand Down
1 change: 1 addition & 0 deletions test/tree-sitter/base-image.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mcr.microsoft.com/devcontainers/base:ubuntu
5 changes: 5 additions & 0 deletions test/tree-sitter/install_tree_sitter_latest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
set -e

# Scenario: install latest tree-sitter CLI only (no grammars)
# No additional scenario-specific checks beyond test.sh
16 changes: 16 additions & 0 deletions test/tree-sitter/install_tree_sitter_with_all_grammars.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
set -e

# Scenario: install tree-sitter with all grammars
source "$(dirname "$0")/test.sh"

GRAMMARS="php typescript javascript go dart python yaml"
for grammar in $GRAMMARS; do
if [ ! -f "${TREE_SITTER_DIR}/${grammar}.so" ]; then
echo "FAIL: ${grammar} grammar not found at ${TREE_SITTER_DIR}/${grammar}.so"
exit 1
fi
echo "PASS: ${grammar} grammar installed"
done

echo "==> All grammar tests passed"
12 changes: 12 additions & 0 deletions test/tree-sitter/install_tree_sitter_with_php.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
set -e

# Scenario: install tree-sitter with PHP grammar
source "$(dirname "$0")/test.sh"

# PHP grammar .so must exist
if [ ! -f "${TREE_SITTER_DIR}/php.so" ]; then
echo "FAIL: PHP grammar not found at ${TREE_SITTER_DIR}/php.so"
exit 1
fi
echo "PASS: PHP grammar installed at ${TREE_SITTER_DIR}/php.so"
30 changes: 30 additions & 0 deletions test/tree-sitter/scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"install_tree_sitter_latest": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"tree-sitter": {}
}
},
"install_tree_sitter_with_php": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"tree-sitter": {
"grammarPhp": true
}
}
},
"install_tree_sitter_with_all_grammars": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"tree-sitter": {
"grammarPhp": true,
"grammarTypescript": true,
"grammarJavascript": true,
"grammarGo": true,
"grammarDart": true,
"grammarPython": true,
"grammarYaml": true
}
}
}
}
31 changes: 31 additions & 0 deletions test/tree-sitter/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
set -e

# Devcontainer feature test — run by devcontainers/action in CI
# Verifies Tree-sitter CLI is correctly installed and on PATH.

echo "==> Testing Tree-sitter feature..."

# tree-sitter binary must be on PATH
if ! command -v tree-sitter &>/dev/null; then
echo "FAIL: tree-sitter not found on PATH"
exit 1
fi
echo "PASS: tree-sitter on PATH ($(command -v tree-sitter))"

# tree-sitter --version must succeed
TS_VERSION=$(tree-sitter --version 2>&1)
if [ -z "$TS_VERSION" ]; then
echo "FAIL: tree-sitter --version returned empty output"
exit 1
fi
echo "PASS: tree-sitter --version => ${TS_VERSION}"

# TREE_SITTER_DIR env var must be set
if [ -z "$TREE_SITTER_DIR" ]; then
echo "FAIL: TREE_SITTER_DIR not set"
exit 1
fi
echo "PASS: TREE_SITTER_DIR=${TREE_SITTER_DIR}"

echo "==> All Tree-sitter base tests passed"