nvim fuzzy file + symbol search for Swift (fzf.vim + universal-ctags)
Replicates Xcode's Cmd+Shift+O ("Open Quickly") in nvim: Ctrl-P fuzzy-finds
files, ,O/,t fuzzy-find symbols (classes, structs, enums, protocols,
extensions, funcs, vars, enum cases, typealiases) project-wide / in the
current buffer.
Config lives in two global, machine-level locations, so once set up they
apply to every Swift project on the machine:
~/.config/nvim/init.vim
~/.ctags.d/swift.ctags
Only the tags file itself (tags, generated inside each project root) is
per-project.
Why this needs custom setup (root causes)
- macOS ships a BSD
ctags stub at /usr/bin/ctags (forwards to Xcode's
ancient ctags via xcrun) that shadows Homebrew's modern ctags in most
shell PATHs, and it lacks -R (recursive) support entirely.
- Even Homebrew's
universal-ctags bottle has no Swift language parser
compiled in — ctags --list-languages will not list Swift. It silently
tags zero symbols from .swift files with no error.
- Because of #1, fzf.vim's
:Tags/:BTags may pick up the broken ctags,
or (because of #2) generate an empty/useless tags file. If a stale tags
file already exists in the project root, fzf.vim won't re-prompt to
regenerate it — it just keeps using the bad file.
Setup steps (run once per machine)
Install fzf and universal-ctags:
brew install fzf universal-ctags
Confirm Homebrew's ctags actually has Swift support — it won't, by
default:
/opt/homebrew/bin/ctags --list-languages | grep -i swift
If empty (expected), continue to step 3.
Create ~/.ctags.d/swift.ctags (ctags reads every .ctags file in this
dir automatically, for every project). Use PCRE2 ({pcre2} suffix on
each rule) since the default POSIX regex engine doesn't support
non-capturing groups (?:...):
--langdef=Swift
--langmap=Swift:.swift
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?(?:final[ \t]+)?class[ \t]+([a-zA-Z0-9_]+)/\1/c,classes/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?struct[ \t]+([a-zA-Z0-9_]+)/\1/s,structs/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?enum[ \t]+([a-zA-Z0-9_]+)/\1/e,enums/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?protocol[ \t]+([a-zA-Z0-9_]+)/\1/p,protocols/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?extension[ \t]+([a-zA-Z0-9_.]+)/\1/x,extensions/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open|static|final|override|mutating|convenience|required|class)[ \t]+)*func[ \t]+([a-zA-Z0-9_]+)/\1/f,functions/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open|static|final|override|lazy|weak)[ \t]+)*(?:let|var)[ \t]+([a-zA-Z0-9_]+)/\1/v,variables/{pcre2}
--regex-Swift=/^[ \t]*case[ \t]+([a-zA-Z0-9_]+)/\1/m,enumcase/{pcre2}
--regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?typealias[ \t]+([a-zA-Z0-9_]+)/\1/t,typealiases/{pcre2}
Verify: /opt/homebrew/bin/ctags --list-languages | grep -i swift should
now print Swift.
Edit ~/.config/nvim/init.vim:
- Add fzf plugins to the vim-plug block:
Plug '/opt/homebrew/opt/fzf'
Plug 'junegunn/fzf.vim'
- Near the top of the file (before anything else runs), prepend
Homebrew's bin dir to nvim's own
$PATH so nvim never resolves to the
broken system ctags, regardless of the user's shell PATH order:if isdirectory('/opt/homebrew/bin')
let $PATH = '/opt/homebrew/bin:' . $PATH
endif
- Add keybindings + restrict tag generation to actual code languages
(keeps markdown/svg/yaml/json prose and data out of symbol search
results):
nnoremap <C-p> :Files<CR>
nnoremap <leader>O :Tags<CR>
nnoremap <leader>t :BTags<CR>
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
let g:fzf_tags_command = 'ctags -R --languages=Swift,C,C++'
(Check for an existing <leader>o/<leader>t mapping conflict first —
e.g. this user already had <leader>o bound to spell-check toggle, so
,t/,O were chosen instead of reusing o.)
- Run
nvim --headless "+PlugInstall --sync" +qa to install the new
plugins.
Setup steps (run once per new Swift project)
cd into the project and open nvim.
- Run
,O (or :Tags). Since no tags file exists yet, fzf.vim prompts
tags not found. Generate? (y/N) — answer y. This runs the
g:fzf_tags_command above and writes tags to the project root.
- Add
tags to that project's .gitignore — it's a generated file and
should never be committed.
Troubleshooting
- "Failed to create tags" / no results despite symbol existing: run
/opt/homebrew/bin/ctags --list-languages | grep -i swift — if empty,
step 3 (the custom swift.ctags langdef) is missing or has a syntax
error. Check for ctags: Warning: ... Failed in compiling the regex pattern on stderr, which means a rule is missing its {pcre2} flag.
- Still no results after fixing the parser: check for a stale
tags
file already sitting in the project root (ls tags) generated before the
fix — delete it and regenerate manually:rm -f tags && /opt/homebrew/bin/ctags -R --languages=Swift,C,C++ .
- Verify which ctags a plain shell resolves to (may differ from what
nvim uses, since nvim's
$PATH is patched in init.vim):which -a ctags
- Sanity-check a single file directly:
/opt/homebrew/bin/ctags -f - path/to/File.swift
1---2name: nvim-swift-fuzzy-search3description: Sets up (or repairs) Xcode-Cmd+Shift+O-style fuzzy file/symbol search in nvim for Swift projects, using fzf.vim + universal-ctags with a custom Swift tag parser. Trigger when the user wants fuzzy file/symbol search in nvim, mentions Ctrl-P/:Tags/:Files not working, ctags not finding Swift symbols, or asks to replicate this setup on a new machine/project.4---56# nvim fuzzy file + symbol search for Swift (fzf.vim + universal-ctags)78Replicates Xcode's Cmd+Shift+O ("Open Quickly") in nvim: `Ctrl-P` fuzzy-finds9files, `,O`/`,t` fuzzy-find symbols (classes, structs, enums, protocols,10extensions, funcs, vars, enum cases, typealiases) project-wide / in the11current buffer.1213Config lives in two **global, machine-level** locations, so once set up they14apply to every Swift project on the machine:15- `~/.config/nvim/init.vim`16- `~/.ctags.d/swift.ctags`1718Only the tags file itself (`tags`, generated inside each project root) is19per-project.2021## Why this needs custom setup (root causes)22231. macOS ships a BSD `ctags` stub at `/usr/bin/ctags` (forwards to Xcode's24 ancient ctags via xcrun) that shadows Homebrew's modern `ctags` in most25 shell PATHs, and it lacks `-R` (recursive) support entirely.262. Even Homebrew's `universal-ctags` bottle has **no Swift language parser27 compiled in** — `ctags --list-languages` will not list Swift. It silently28 tags zero symbols from `.swift` files with no error.293. Because of #1, fzf.vim's `:Tags`/`:BTags` may pick up the broken ctags,30 or (because of #2) generate an empty/useless tags file. If a stale `tags`31 file already exists in the project root, fzf.vim won't re-prompt to32 regenerate it — it just keeps using the bad file.3334## Setup steps (run once per machine)35361. Install fzf and universal-ctags:37 ```38 brew install fzf universal-ctags39 ```40412. Confirm Homebrew's ctags actually has Swift support — it won't, by42 default:43 ```44 /opt/homebrew/bin/ctags --list-languages | grep -i swift45 ```46 If empty (expected), continue to step 3.47483. Create `~/.ctags.d/swift.ctags` (ctags reads every `.ctags` file in this49 dir automatically, for every project). Use PCRE2 (`{pcre2}` suffix on50 each rule) since the default POSIX regex engine doesn't support51 non-capturing groups `(?:...)`:52 ```53 --langdef=Swift54 --langmap=Swift:.swift55 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?(?:final[ \t]+)?class[ \t]+([a-zA-Z0-9_]+)/\1/c,classes/{pcre2}56 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?struct[ \t]+([a-zA-Z0-9_]+)/\1/s,structs/{pcre2}57 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?enum[ \t]+([a-zA-Z0-9_]+)/\1/e,enums/{pcre2}58 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?protocol[ \t]+([a-zA-Z0-9_]+)/\1/p,protocols/{pcre2}59 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?extension[ \t]+([a-zA-Z0-9_.]+)/\1/x,extensions/{pcre2}60 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open|static|final|override|mutating|convenience|required|class)[ \t]+)*func[ \t]+([a-zA-Z0-9_]+)/\1/f,functions/{pcre2}61 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open|static|final|override|lazy|weak)[ \t]+)*(?:let|var)[ \t]+([a-zA-Z0-9_]+)/\1/v,variables/{pcre2}62 --regex-Swift=/^[ \t]*case[ \t]+([a-zA-Z0-9_]+)/\1/m,enumcase/{pcre2}63 --regex-Swift=/^[ \t]*(?:(?:public|private|internal|fileprivate|open)[ \t]+)?typealias[ \t]+([a-zA-Z0-9_]+)/\1/t,typealiases/{pcre2}64 ```65 Verify: `/opt/homebrew/bin/ctags --list-languages | grep -i swift` should66 now print `Swift`.67684. Edit `~/.config/nvim/init.vim`:69 - Add fzf plugins to the vim-plug block:70 ```71 Plug '/opt/homebrew/opt/fzf'72 Plug 'junegunn/fzf.vim'73 ```74 - Near the top of the file (before anything else runs), prepend75 Homebrew's bin dir to nvim's own `$PATH` so nvim never resolves to the76 broken system ctags, regardless of the user's shell PATH order:77 ```vim78 if isdirectory('/opt/homebrew/bin')79 let $PATH = '/opt/homebrew/bin:' . $PATH80 endif81 ```82 - Add keybindings + restrict tag generation to actual code languages83 (keeps markdown/svg/yaml/json prose and data out of symbol search84 results):85 ```vim86 nnoremap <C-p> :Files<CR>87 nnoremap <leader>O :Tags<CR>88 nnoremap <leader>t :BTags<CR>89 let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }90 let g:fzf_tags_command = 'ctags -R --languages=Swift,C,C++'91 ```92 (Check for an existing `<leader>o`/`<leader>t` mapping conflict first —93 e.g. this user already had `<leader>o` bound to spell-check toggle, so94 `,t`/`,O` were chosen instead of reusing `o`.)95 - Run `nvim --headless "+PlugInstall --sync" +qa` to install the new96 plugins.9798## Setup steps (run once per new Swift project)991001. `cd` into the project and open nvim.1012. Run `,O` (or `:Tags`). Since no `tags` file exists yet, fzf.vim prompts102 `tags not found. Generate? (y/N)` — answer `y`. This runs the103 `g:fzf_tags_command` above and writes `tags` to the project root.1043. Add `tags` to that project's `.gitignore` — it's a generated file and105 should never be committed.106107## Troubleshooting108109- **"Failed to create tags" / no results despite symbol existing**: run110 `/opt/homebrew/bin/ctags --list-languages | grep -i swift` — if empty,111 step 3 (the custom `swift.ctags` langdef) is missing or has a syntax112 error. Check for `ctags: Warning: ... Failed in compiling the regex113 pattern` on stderr, which means a rule is missing its `{pcre2}` flag.114- **Still no results after fixing the parser**: check for a stale `tags`115 file already sitting in the project root (`ls tags`) generated before the116 fix — delete it and regenerate manually:117 ```118 rm -f tags && /opt/homebrew/bin/ctags -R --languages=Swift,C,C++ .119 ```120- **Verify which ctags a plain shell resolves to** (may differ from what121 nvim uses, since nvim's `$PATH` is patched in `init.vim`):122 ```123 which -a ctags124 ```125- **Sanity-check a single file directly**:126 ```127 /opt/homebrew/bin/ctags -f - path/to/File.swift128 ```