# Nvim Swift Fuzzy Search

> 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.

- Skill: `sohandotgit/nvim-swift-fuzzy-search` (Agent Skill)
- Install (CLI): `npx skillmds@latest add sohandotgit/nvim-swift-fuzzy-search`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sohandotgit/nvim-swift-fuzzy-search/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: sohandotgit (https://skillmd.com/u/sohandotgit)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/sohandotgit/nvim-swift-fuzzy-search

---


# 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)

1. 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.
2. 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.
3. 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)

1. Install fzf and universal-ctags:
   ```
   brew install fzf universal-ctags
   ```

2. 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.

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`.

4. 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:
     ```vim
     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):
     ```vim
     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)

1. `cd` into the project and open nvim.
2. 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.
3. 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
  ```

