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'