Commit 5e93e06
Eric Bower
·
2026-10-13 14:19:32 -0400 EDT
parent d1a84a9
fix: ctags
3 files changed,
+109,
-0
| ... | ... | @@ -11,6 +11,16 @@ generated=0 | |
| 11 | 11 | # Start from a clean slate so repeated runs don't accumulate duplicate tags. | |
| 12 | 12 | rm -f "${target}" | |
| 13 | 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. | |
| 16 | + | absolutize_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 | + | ||
| 14 | 24 | # 1. Go modules | |
| 15 | 25 | if [[ -f "go.mod" ]] && command -v go >/dev/null 2>&1; then | |
| 16 | 26 | echo "Resolving Go dependencies..." |
| ... | ... | @@ -42,12 +52,14 @@ if [[ -f "build.zig.zon" ]] || [[ -d "zig-pkg" ]] || [[ -f ".generated-sources" | |
| 42 | 52 | # .zig-cache and are not reachable from zig-pkg, so we index them directly. | |
| 43 | 53 | if [[ -f ".generated-sources" ]]; then | |
| 44 | 54 | while IFS= read -r gf; do | |
| 55 | + | gf="${gf#./}" | |
| 45 | 56 | [[ -n "$gf" && -f "$gf" ]] && zig_inputs+=("$gf") | |
| 46 | 57 | done < .generated-sources | |
| 47 | 58 | fi | |
| 48 | 59 | ||
| 49 | 60 | if [[ ${#zig_inputs[@]} -gt 0 ]]; then | |
| 50 | 61 | readarray -t zig_inputs < <(printf "%s\n" "${zig_inputs[@]}" | sort -u) | |
| 62 | + | absolutize_arr zig_inputs | |
| 51 | 63 | echo "Indexing ${#zig_inputs[@]} Zig sources into ${target}..." | |
| 52 | 64 | # NOTE: --languages=Zig (not +Zig) so we don't also index CSS/HTML/JSON/etc. | |
| 53 | 65 | tmp_tags="$(mktemp)" |
| ... | ... | @@ -87,6 +99,7 @@ if [[ -n "${venv_dir}" ]]; then | |
| 87 | 99 | done | |
| 88 | 100 | ||
| 89 | 101 | if [[ ${#sp_dirs[@]} -gt 0 ]]; then | |
| 102 | + | absolutize_arr sp_dirs | |
| 90 | 103 | echo "Indexing Python site-packages into ${target}..." | |
| 91 | 104 | flags=("-f" "${target}" "--languages=Python" "--exclude=*.dist-info" "--exclude=*.egg-info" "--exclude=*/tests/*" "--exclude=*/test/*") | |
| 92 | 105 | [[ $generated -eq 1 ]] && flags+=("-a") |
| ... | ... | @@ -107,6 +120,7 @@ if [[ -f "package.json" && -d "node_modules" ]]; then | |
| 107 | 120 | done < <(find node_modules -maxdepth 5 -name "*.d.ts" ! -path "*/node_modules/@types/*" 2>/dev/null) | |
| 108 | 121 | ||
| 109 | 122 | if [[ ${#ts_targets[@]} -gt 0 ]]; then | |
| 123 | + | absolutize_arr ts_targets | |
| 110 | 124 | echo "Indexing TypeScript types into ${target}..." | |
| 111 | 125 | flags=("-f" "${target}" "--languages=TypeScript,JavaScript" "--exclude=*/test/*" "--exclude=*/tests/*") | |
| 112 | 126 | [[ $generated -eq 1 ]] && flags+=("-a") |
+59,
-0
| ... | ... | @@ -0,0 +1,59 @@ | |
| 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). | |
| 14 | + | set -euo pipefail | |
| 15 | + | ||
| 16 | + | awk ' | |
| 17 | + | # Extract a plausible declaration name from a line. | |
| 18 | + | function 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 | + | ' |
+36,
-0
| ... | ... | @@ -149,6 +149,42 @@ define-command ctags-file-symbols -docstring "Browse and jump to functions in cu | |
| 149 | 149 | } | |
| 150 | 150 | } | |
| 151 | 151 | ||
| 152 | + | # Override the stock ctags-search. The stock version jumps pattern-based tags | |
| 153 | + | # via a regex search that always lands on the FIRST match, so overloaded / | |
| 154 | + | # duplicate names (e.g. wayland's ~20 setHandler, all identical lines) collapse | |
| 155 | + | # to a single result. This version jumps by the exact `line:` extension field and | |
| 156 | + | # labels each item with its enclosing type (via tag-scope) plus line number, so | |
| 157 | + | # every overload is distinguishable and reachable, e.g. | |
| 158 | + | # setHandler: Surface @ line 4960 | |
| 159 | + | define-command -override ctags-search -params ..1 -docstring "Jump to a symbol's definition (line-accurate, lists overloads)" %[ | |
| 160 | + | require-module menu | |
| 161 | + | evaluate-commands %sh[ | |
| 162 | + | realpath() { ( cd "$(dirname "$1")"; printf "%s/%s\n" "$(pwd -P)" "$(basename "$1")" ) } | |
| 163 | + | export tagname="${1:-${kak_selection}}" | |
| 164 | + | eval "set -- $kak_quoted_opt_ctagsfiles" | |
| 165 | + | for candidate in "$@"; do | |
| 166 | + | [ -f "$candidate" ] && realpath "$candidate" | |
| 167 | + | done | awk '!x[$0]++' | while read -r tags; do | |
| 168 | + | printf '!TAGROOT\t%s\n' "$(realpath "${tags%/*}")/" | |
| 169 | + | readtags -e -n -t "$tags" "$tagname" | |
| 170 | + | done | tag-scope | awk -F '\t' ' | |
| 171 | + | /^!TAGROOT\t/ { tagroot = $2; next } | |
| 172 | + | { | |
| 173 | + | name = $1; path = $2 | |
| 174 | + | line = ""; scope = "" | |
| 175 | + | for (i = 4; i <= NF; i++) { if ($i ~ /^line:/) line = substr($i, 6); else if ($i ~ /^scope:/) scope = substr($i, 7) } | |
| 176 | + | if (line == "") next | |
| 177 | + | menu_item = name; gsub(/!/, "!!", menu_item) | |
| 178 | + | menu_info = (scope != "" ? scope " @ line " : "line ") line; gsub(/!/, "!!", menu_info) | |
| 179 | + | edit_path = path; if (edit_path !~ /^\//) edit_path = tagroot edit_path | |
| 180 | + | gsub(/!/, "!!", edit_path); gsub(/#/, "##", edit_path); gsub(/&/, "&&", edit_path); gsub(/\|/, "||", edit_path) | |
| 181 | + | out = out "%!" menu_item ": " menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|" line "gx| & catch %& fail unable to find tag & # !" | |
| 182 | + | } | |
| 183 | + | END { print (length(out) == 0 ? "fail no such tag " ENVIRON["tagname"] : "menu -auto-single " out) } | |
| 184 | + | ' | |
| 185 | + | ] | |
| 186 | + | ] | |
| 187 | + | ||
| 152 | 188 | hook global WinCreate .* %{ | |
| 153 | 189 | ctags-enable-autoinfo | |
| 154 | 190 | } |