Last active
July 26, 2026 13:54
-
-
Save ilan-schemoul/8587bff1c90cf6dd0408a8a1a3fd6d41 to your computer and use it in GitHub Desktop.
Fish inside Nvim (how to and optimisations)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Rest of this file will take fish as the Shell, it works roughly the same with other shell though. | |
| NOTE: if you add `set shell=fish` in nvim there's actually a third fish process. It becomes fish1->nvim(->starts its internal fish shell (fish2))->fish3. We are in fish 2 if $NVIM_LOG_FILE is defined but not $NVIM (from my limited tests). | |
| # Fish sandwich | |
| When I start a shell I want it to run inside Nvim, so at any time I can access | |
| nvim (with houdini plugin I can just type hj in my terminal emulator to exit terminal mode, and be in normal nvim mode). | |
| It is not possible, AFAIK, to directly start Nvim from Kitty (or other | |
| emulator). So I start Fish, which starts Nvim, which starts fish again. | |
| This is what I call: | |
| - Fish stage 1 | |
| - Nvim | |
| - Fish stage 2 | |
| # Code snippet | |
| Kitty calls fish: `shell fish` in kitty.conf | |
| Fish stage 1 calls nvim: | |
| ```fish | |
| # Only put stuff you need in Nvim here (it will be executed twice so it's costly). | |
| if status is-interactive | |
| and test -z "$NVIM" | |
| nvim -c ":term fish" | |
| ``` | |
| Nvim calls Fish stage 2: see snippet above (it's the `-c` part) | |
| # Perf | |
| As there is 3 steps involved it can be slow to startup. When CPU is busy, | |
| without optimisations it can take multiple seconds. | |
| ## Benchmark | |
| - Install hyperfine | |
| - Run a fish terminal without Nvim | |
| For fish stage 1 you need to add a exit 0 before the if block that starts nvim | |
| (to emulate the fact stage 1 should never execute the rest of the file) | |
| Fish stage 1: `hyperfine fish -c 'exit'` (as -c is non interactive command, nvim won't be started (see the code snippet)) | |
| Nvim: `hyperfine "NVIM='fake' fish -c 'exit'"` (add -u none to benchmark without config) | |
| Fish stage 2: inside nvim `hyperfine "fish -c 'exit'"` | |
| Results on my mac: | |
| - Fish stage 1: 3ms | |
| - Nvim: 70ms (100 plugins in total, but heavy lazy loading, see below) | |
| - Fish stage 2: 35ms | |
| To profile each step: | |
| - Nvim, use lazy profiler if you use Lazy. | |
| - Fish: | |
| - Run fish with --profile-startup /tmp/startup.prof. As explained | |
| previously, depending on if NVIM variable is set you are testing stage 1 | |
| or stage 2. | |
| - sort -nk2 /tmp/startup.prof to read the data sorted. Left column is own time, | |
| right column is own time + children time. | |
| # Optim | |
| ## Fish stage 1 | |
| I added a check in each conf.d file if "$NVIM" is defined we `return` to | |
| avoid reading the file for stage 1. Except the bits Nvim might need, which we put | |
| above the guard clause (e.g.: add brew in path so nvim has access to binaries). | |
| Same for config.fish, everything is put after exec nvim bloc except if needed by | |
| nvim. | |
| With these two things, we make fish stage 1 faster. | |
| ## Nvim | |
| Another huge optim that almost doubled nvim startup time, is to have defined a | |
| user event that only gets triggered once we have loaded our first file. Most | |
| plugins use that event. It means that as long as we are in terminal mode and | |
| haven't opened our first file yet most plugins are not loaded. | |
| See https://github.com/ilan-schemoul/nvim-config/blob/f2014aef160dbef564b3828c743e2b722dde72d2/nvim/lua/config/lazy.lua#L17. | |
| Then just use NormalBufferEnter as the `event` for each plugin you don't need in terminal mode. | |
| ## Fish (stage 1 and 2) | |
| I have noticed various things that can be slow. For example, brew default | |
| command to source is very slow (20ms). I executed it once manually, read the | |
| variables it exports and manually have run fish commands (such as set -U or | |
| fish_add_paths) that save permanently the variables. | |
| fish_add_paths is very slow if we call it with a path not already in $PATH. If a | |
| file does not exist, it means the path is never added to $PATH and we do the | |
| check on disk everytime. It is horrible for the perfs. | |
| I removed every call to fish_add_paths (as they are permanent you do not need | |
| to execute them everytime). It means now I have on new PC to call | |
| fish_add_paths manually once though. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment