main dotfiles / bin / ctags-deps
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