main dotfiles / bin / tag-scope
Eric Bower  ·  2026-10-13
 1#!/usr/bin/env bash
 2# tag-scope: read tags (readtags -e -n format) on stdin and append a `scope:`
 3# extension field to each tag, giving the name of the enclosing declaration.
 4#
 5# ctags doesn't record scope for Zig (or most languages) for methods that share
 6# a name (e.g. wayland's ~20 `setHandler`), so go-to-definition can't tell them
 7# apart. This recovers the enclosing type by brace-matching: for a tag at line
 8# N, it finds the nearest preceding line whose `{` opens the block still active
 9# at N, and extracts the declared name from that line.
10#
11# Each source file is read once (cached). Pseudo-tags (!...) and tags without a
12# line number pass through unchanged. For languages/structures it can't parse,
13# the scope is empty and the tag is emitted with `scope:` (harmless).
14set -euo pipefail
15
16awk '
17# Extract a plausible declaration name from a line.
18function extract(line,   s) {
19  if (match(line, /pub[ \t]+(const|var)?[ \t]+[A-Za-z_][A-Za-z0-9_]*/)) {
20    s = substr(line, RSTART, RLENGTH); sub(/pub[ \t]+(const|var)?[ \t]+/, "", s); return s
21  }
22  if (match(line, /fn[ \t]+[A-Za-z_][A-Za-z0-9_]*/)) {
23    s = substr(line, RSTART, RLENGTH); sub(/fn[ \t]+/, "", s); return s
24  }
25  if (match(line, /(struct|class|union|type|enum)[ \t]+[A-Za-z_][A-Za-z0-9_]*/)) {
26    s = substr(line, RSTART, RLENGTH); sub(/^(struct|class|union|type|enum)[ \t]+/, "", s); return s
27  }
28  return ""
29}
30# Pass pseudo-tags through untouched.
31$1 ~ /^!/ { print; next }
32{
33  ln = ""
34  for (f = 1; f <= NF; f++) if ($f ~ /^line:/) ln = substr($f, 6)
35  if (ln == "") { print; next }
36
37  file = $2; N = ln + 0
38  if (!(file in loaded)) {
39    cum = 0
40    while ((getline l < file) > 0) {
41      cnt[file]++
42      o = gsub(/{/, "&", l); c = gsub(/}/, "&", l)
43      sd[file, cnt[file]] = cum
44      src[file, cnt[file]] = l
45      cum += o - c
46    }
47    close(file)
48    loaded[file] = 1
49  }
50
51  scope = ""
52  if (N >= 1 && N <= cnt[file]) {
53    D = sd[file, N]
54    for (L = N - 1; L >= 1; L--)
55      if (sd[file, L] == D - 1 && src[file, L] ~ /\{/) { scope = extract(src[file, L]); break }
56  }
57  print $0 "\tscope:" scope
58}
59'