Eric Bower
·
2026-10-13
1#!/usr/bin/env bash
2set -euo pipefail
3
4if ! command -v ctags >/dev/null 2>&1; then
5 echo "error: ctags not found in PATH" >&2
6 exit 1
7fi
8
9target="tags_deps"
10generated=0
11# Start from a clean slate so repeated runs don't accumulate duplicate tags.
12rm -f "${target}"
13
14# ctags records *relative* input paths with a leading "../" (a Universal Ctags
15# quirk) that editors can't resolve. Feed it absolute paths instead.
16absolutize_arr() {
17 local -n _arr="$1"
18 local i
19 for i in "${!_arr[@]}"; do
20 [[ "${_arr[$i]}" = /* ]] || _arr[i]="$(pwd)/${_arr[i]}"
21 done
22}
23
24# 1. Go modules
25if [[ -f "go.mod" ]] && command -v go >/dev/null 2>&1; then
26 echo "Resolving Go dependencies..."
27 go mod download 2>/dev/null || true
28 dirs="$(go list -m -f '{{if not .Main}}{{.Dir}}{{end}}' all 2>/dev/null | grep -v '^$' || true)"
29 if [[ -n "${dirs}" ]]; then
30 echo "Indexing Go dependencies into ${target}..."
31 flags=("-f" "${target}" "--languages=Go" "--exclude=testdata")
32 [[ $generated -eq 1 ]] && flags+=("-a")
33 echo "${dirs}" | ctags "${flags[@]}" -L -
34 generated=1
35 fi
36fi
37
38# 2. Zig dependencies (build.zig.zon / zig-pkg) + build-generated sources
39if [[ -f "build.zig.zon" ]] || [[ -d "zig-pkg" ]] || [[ -f ".generated-sources" ]]; then
40 echo "Resolving Zig dependencies..."
41 zig_inputs=()
42
43 # Third-party dependency sources
44 if [[ -d "zig-pkg" ]]; then
45 for d in zig-pkg/*; do
46 [[ -d "$d" ]] && zig_inputs+=("$d")
47 done
48 fi
49
50 # Project build-generated sources (e.g. wayland-scanner output) listed by
51 # build.zig in .generated-sources, one absolute path per line. These live in
52 # .zig-cache and are not reachable from zig-pkg, so we index them directly.
53 if [[ -f ".generated-sources" ]]; then
54 while IFS= read -r gf; do
55 gf="${gf#./}"
56 [[ -n "$gf" && -f "$gf" ]] && zig_inputs+=("$gf")
57 done < .generated-sources
58 fi
59
60 if [[ ${#zig_inputs[@]} -gt 0 ]]; then
61 readarray -t zig_inputs < <(printf "%s\n" "${zig_inputs[@]}" | sort -u)
62 absolutize_arr zig_inputs
63 echo "Indexing ${#zig_inputs[@]} Zig sources into ${target}..."
64 # NOTE: --languages=Zig (not +Zig) so we don't also index CSS/HTML/JSON/etc.
65 tmp_tags="$(mktemp)"
66 printf "%s\n" "${zig_inputs[@]}" | ctags \
67 -f "${tmp_tags}" --languages=Zig \
68 --exclude=test --exclude=tests \
69 --exclude=example --exclude=examples \
70 --exclude=doc --exclude=docs \
71 --exclude=zig-out --exclude=build.zig \
72 -L -
73 # Prune to the public interface: keep only tags whose definition line
74 # is a `pub` declaration (private items are unreachable from user code).
75 if [[ $generated -eq 0 ]]; then
76 grep '^!' "${tmp_tags}" >> "${target}" || true
77 fi
78 awk -F'\t' '$2 ~ /\.zig$/ && $3 ~ /(^|[^A-Za-z0-9_])pub /' "${tmp_tags}" >> "${target}"
79 rm -f "${tmp_tags}"
80 generated=1
81 fi
82fi
83
84# 3. Python virtualenv
85venv_dir=""
86if [[ -n "${VIRTUAL_ENV:-}" && -d "${VIRTUAL_ENV}" ]]; then
87 venv_dir="${VIRTUAL_ENV}"
88elif [[ -d ".venv" ]]; then
89 venv_dir=".venv"
90elif [[ -d "venv" ]]; then
91 venv_dir="venv"
92fi
93
94if [[ -n "${venv_dir}" ]]; then
95 echo "Resolving Python dependencies from ${venv_dir}..."
96 sp_dirs=()
97 for sp in "${venv_dir}"/lib/python*/site-packages; do
98 [[ -d "$sp" ]] && sp_dirs+=("$sp")
99 done
100
101 if [[ ${#sp_dirs[@]} -gt 0 ]]; then
102 absolutize_arr sp_dirs
103 echo "Indexing Python site-packages into ${target}..."
104 flags=("-f" "${target}" "--languages=Python" "--exclude=*.dist-info" "--exclude=*.egg-info" "--exclude=*/tests/*" "--exclude=*/test/*")
105 [[ $generated -eq 1 ]] && flags+=("-a")
106 ctags "${flags[@]}" "${sp_dirs[@]}"
107 generated=1
108 fi
109fi
110
111# 4. TypeScript / Node.js
112if [[ -f "package.json" && -d "node_modules" ]]; then
113 echo "Resolving TypeScript / Node dependencies..."
114 ts_targets=()
115 if [[ -d "node_modules/@types" ]]; then
116 ts_targets+=("node_modules/@types")
117 fi
118 while IFS= read -r dts; do
119 [[ -f "$dts" ]] && ts_targets+=("$dts")
120 done < <(find node_modules -maxdepth 5 -name "*.d.ts" ! -path "*/node_modules/@types/*" 2>/dev/null)
121
122 if [[ ${#ts_targets[@]} -gt 0 ]]; then
123 absolutize_arr ts_targets
124 echo "Indexing TypeScript types into ${target}..."
125 flags=("-f" "${target}" "--languages=TypeScript,JavaScript" "--exclude=*/test/*" "--exclude=*/tests/*")
126 [[ $generated -eq 1 ]] && flags+=("-a")
127 printf "%s\n" "${ts_targets[@]}" | ctags "${flags[@]}" -L -
128 generated=1
129 fi
130fi
131
132# 5. Standard library tags for detected languages.
133# Cached in $XDG_CACHE_HOME/tags and shared across projects, so we only
134# generate them when missing (regenerating a whole stdlib is slow). To force a
135# refresh, delete the cached file or run the individual ctags-*-std script.
136cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/tags"
137script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
138std_generated=0
139
140ensure_std() {
141 local lang="$1" script="$2"
142 local std_target="${cache_dir}/${lang}_std.tags"
143 if [[ -f "${std_target}" ]]; then
144 echo "std tags for ${lang} cached ($(du -h "${std_target}" | cut -f1))"
145 std_generated=1
146 return 0
147 fi
148 if [[ -x "${script_dir}/${script}" ]]; then
149 echo "Generating ${lang} std tags..."
150 if "${script_dir}/${script}"; then
151 std_generated=1
152 else
153 echo "warning: failed to generate ${lang} std tags" >&2
154 fi
155 else
156 echo "warning: ${script} not found, skipping ${lang} std tags" >&2
157 fi
158}
159
160if [[ -f "go.mod" ]]; then
161 ensure_std "go" "ctags-go-std"
162fi
163if [[ -f "build.zig" ]]; then
164 ensure_std "zig" "ctags-zig-std"
165fi
166if [[ -n "${VIRTUAL_ENV:-}" && -d "${VIRTUAL_ENV}" ]] || [[ -d ".venv" ]] || [[ -d "venv" ]] \
167 || [[ -f "pyproject.toml" || -f "setup.py" || -f "requirements.txt" ]]; then
168 ensure_std "python" "ctags-python-std"
169fi
170if [[ -f "package.json" ]]; then
171 ensure_std "ts" "ctags-ts-std"
172fi
173
174if [[ $generated -eq 1 ]]; then
175 echo "Saved dependency tags to ${target} ($(du -h "${target}" | cut -f1))"
176fi
177
178if [[ $generated -eq 1 || $std_generated -eq 1 ]]; then
179 exit 0
180fi
181
182echo "No third-party dependencies or std libs detected in current directory."
183exit 1