Make your setup truly cross-platform
Once you have taken the time to setup Vim exactly the way you want, you might
still encounter configuration issues. My whole ~/.vim/
directory is under version
control so I can just clone it on any computer I want my familiar Vim
experience.
But I use Windows computers and Linux computers. Even within the same environment, I might have different dependencies installed for the plugins I try to import. Also, from time to time I like to use Neovim too, to try out a few unique features.
All the tips in this article gravitate around being able to use if statements in your startup scripts.
Environment specific settings
To make environment specific settings we need to know the environment we are
running on.
We can ask vim directly if it was compiled for Windows, otherwise a system call
to uname
will give the running environment. The function below returns a string
containing the value of the running environment.
function! WhichEnv() abort
if has('win64') || has('win32') || has('win16')
return 'WINDOWS'
else
return toupper(substitute(system('uname'), '\n', '', ''))
endif
endfunction
"""""""""""""""""""""""""""""
" Later use in another file "
"""""""""""""""""""""""""""""
if (WhichEnv() =~# 'WINDOWS')
" Enable Windows specific settings/plugins
else if (WhichEnv() =~# 'LINUX')
" Enable Linux specific settings/plugins
else if (WhichEnv() =~# 'DARWIN')
" Enable MacOS specific settings/plugins
else
" Other cases I can't think of like MINGW
endif
Note that vim also has so-called features (the arguments in the has
function) for checking respectively 'osx'
and ‘osx_darwin
’, but after having
spoken with a MacOS user (I don’t use it personnally), there seems to be cases
where using these flags for MacOS detection is not working as
one would guess.
Intermission: external shell calls optimizations
External shell calls are costly, mostly because of the context switching they require. They are not very long, but one of the big advantages of vim is the very quick startup time, and having too many external calls in your startup will definitely have consequences on your experience.
To illustrate this, let’s compare the startup time of Vim with a one-liner vimrc, where:
one case has
let g:dummy_string = 'dummy string'
(no external shell call),the other case has
let g:dummy_string = system('date')
(one external shell call)
The command I used to compare the startup times was:
$ vim --noplugin -u one_liner_vimrc --startuptime startup.txt
and here is the final result:
Condition | Duration in milliseconds |
---|---|
No external call | 002.369 |
External call | 093.497 |
The 90 ms difference in startup time is entirely due to the external system call
(see :h startuptime
for help on the syntax):
# No external call
001.246 000.021 000.021: sourcing no_externalcall_vimrc
# External call
092.540 088.869 088.869: sourcing externalcall_vimrc
Caching the results from any external system call as
much as possible is important when crafting flexible .vim/
startup scripts.
Here is the final function I use (and call in my other scripts) to know my
environment. The result of system()
is stored in a global variable and used
in all scripts.
"""""""""""""""""""""""""""""
" vimrc "
"""""""""""""""""""""""""""""
" The function needs to be near the top since it has to be known for later use
" Sets only once the value of g:env to the running environment
" from romainl
" https://gist.github.com/romainl/4df4cde3498fada91032858d7af213c2
function! Config_setEnv() abort
if exists('g:env')
return
endif
if has('win64') || has('win32') || has('win16')
let g:env = 'WINDOWS'
else
let g:env = toupper(substitute(system('uname'), '\n', '', ''))
endif
endfunction
"""""""""""""""""""""""""""""
" Later use in another file "
"""""""""""""""""""""""""""""
" I can call this function before every environment specific block with the
" early return branch.
call Config_setEnv()
if (g:Env =~# 'WINDOWS')
" Enable Windows specific settings/plugins
else if (g:Env =~# 'LINUX')
" Enable Linux specific settings/plugins
else if (g:Env =~# 'DARWIN')
" Enable MacOS specific settings/plugins
else
" Other cases I can't think of like MINGW
endif
Host specific settings
We can use exactly the same method for host specific settings.
Linux provides hostname so we can use
the same function as before, replacing only the toupper...
line with
system('hostname')
, and storing it in another g:
variable like g:Hostname
.
This method should also work on Windows, since it also provides hostname, but I have not tested it yet.
EDIT : Just as shown below with the executable()
function, Vim provides a
hostname()
vimscript function which takes away the need for system calls. That
is better for performance and portability; so if your hostname does not go past
256 characters, calling hostname()
is better than calling system('hostname')
.
As always with Vim, check :h hostname()
for further information
In order to show how this can be useful I will have to present a little my work environment.
I am currently a PhD student in computational mechanics, which is one heavy user of High Performance Computing. The laboratory has a Linux-powered cluster on which all the heavy simulations are run. The software we use is written in C++ and we built a DSL to communicate input parameters through plain-text files to the software.
This means I need to edit text from multiple places on multiple machines for my work:
I might want to edit files directly on my office Windows machine. This machine is a little beefy and I can use it to test locally developments which need more computational power to be run.
I might want to edit files stored on the cluster from my office Windows machine (with ssh). This is useful to work on the code and/or launch tests directly in the correct environment.
I might want to edit files stored on the cluster from my laptop running Linux (with ssh). This is useful when I want to change quickly simulation parameters.
I might want to edit files directly on my laptop. This is where I work on code the most.
Therefore, I use Vim to edit text in two Linux environments (my laptop and the
front node of the cluster), but I do have specific issues related to the way I
access the files (either “natively” or through ssh using a Windows client are
the two extremes). So in the following snippet, I use the “host specific” method
to disable X server connection when working on the cluster (Putty used to try to
connect and wait for a timeout, leading to startup times of 3-5 seconds), and
to add the FZF directory to the
runtimepath, since I had to install fzf in my $HOME
directory on this machine.
"""""""""""""""""""""""""""""
" vimrc "
"""""""""""""""""""""""""""""
" Sets only once the value of g:host to the output of hostname
function! Config_setHost() abort
if exists('g:Hostname')
return
endif
let g:Hostname = system('hostname')
endfunction
call Config_setHost()
if g:Hostname =~? 'front'
set clipboard=exclude:.*
set runtimepath+=~/.fzf
endif
Host specific settings are good when you know you’re only cloning your ~/.vim/
directory in a few computers on which you know what is installed. Using this to
differentiate between 50 hosts means you will need very long if statements which
get quickly hard to read.
I still find this useful for clipboard handling or other purely host-specific settings.
Security note: as you can see in the last snippet, you have to put the hostname in your configuration in order to make these specific settings. This information will then be available to anyone who can see your configuration files on the internet. If this is an issue (especially regarding version control on online git repositories), I think the best thing to do is to:
keep these if statements in separate
*.vim
files,move these
*.vim
files in a specific directory under~/.vim/
likehost_settings
,:runtime
the settings in the version controlled file.
Eventually the configuration looks like this:
""""""""""""""""""""""""""""""
" vimrc (version-controlled) "
""""""""""""""""""""""""""""""
" Sets only once the value of g:host to the output of hostname
function! Config_setHost() abort
if exists('g:Hostname')
return
endif
let g:host = system('hostname')
endfunction
call Config_setHost()
runtime host_settings/34.vim
"""""""""""""""""""""""""""""
" .gitignore "
"""""""""""""""""""""""""""""
# Ignore the host_settings directory in version control
host_settings/
" All files below are now private
"""""""""""""""""""""""""""""
" host_settings/34.vim "
"""""""""""""""""""""""""""""
if g:Hostname =~? 'front'
set clipboard=exclude:.*
set runtimepath+=~/.fzf
endif
If all snippets can be run at the same location, you can even use globs to hide even file names:
""""""""""""""""""""""""""""""
" vimrc (version-controlled) "
""""""""""""""""""""""""""""""
" Sets only once the value of g:host to the output of hostname
function! Config_setHost() abort
if exists('g:Hostname')
return
endif
let g:host = system('hostname')
endfunction
call Config_setHost()
runtime! host_settings/*.vim " Beware of the '!', it is necessary
"""""""""""""""""""""""""""""
" .gitignore "
"""""""""""""""""""""""""""""
# Ignore the host_settings directory in version control
host_settings/
" All files below are now private
""""""""""""""""""""""""""""""""""""""""
" host_settings/clipboard_settings.vim "
""""""""""""""""""""""""""""""""""""""""
if g:Hostname =~? 'front'
set clipboard=exclude:.*
endif
""""""""""""""""""""""""""""""""""
" host_settings/fzf_settings.vim "
""""""""""""""""""""""""""""""""""
if g:Hostname =~? 'front'
set runtimepath+=~/.fzf
endif
Dependencies specific settings
Even on the same environment, dependencies might not be fulfilled on all the target machines. These dependencies can be separated into two categories, Vim’s features and external dependencies. The difference I make between these two is that features are defined at compilation time in Vim and that dependencies are external to Vim’s compilation.
Vim keeps track of its own feature set, defined at compile time. Therefore you
can directly use vimscript to know if Vim has a feature or not, using the
has()
function.
See :h has()
for all the features you can test for
directly within vim.
if has('cscope')
" Enable all the plugins or change settings to use cscope support
endif
For external dependencies (like the linters you might want to set as 'makeprg'
or the LSP servers you want to start for a project), using executable()
instead of using a call to which is very
important, as it is way faster than system()
. I ran the same test case as
before with a vimrc which only include an executable()
call:
if executable('rg')
let g:string_date = 'dummy string'
" usually the line here is set grepprg=rg\ --vimgrep
endif
and the results are almost the same as the no external call case:
# Important lines only
001.991 000.136 000.136: sourcing exec_vimrc
003.383 000.005: --- VIM STARTED ---
Bonus round: Vim 8+ and Neovim compatibility
Vim 8+ is important because I make heavy usage of the package feature for this adaptation.
First step is to symlink the directories of course. We only want one copy of the
~/.vim/
directory on the system, Vim does not care about init.vim
and Neovim does
not care about vimrc
.
After a few updates I made in my plugins and/or colorschemes, I noticed I
always had two files to change: init.vim
and vimrc
. I still want to keep the
files different because there are a few settings which are actually specific to
one software, but duplicating changes is a code smell.
My solution is to use
:runtime
heavily and externalize all the common parts of my old vimrc
.
:runtime
will look for files to source with the given name in your
'runtimepath'
and will source the first one it finds. Choosing a “unique”
directory for the sourced files (like settings/
) allows to store them all with
any name as long as they are in the 'runtimepath'
. I choose to leave them
directly in the ~/.vim/
directory to have them under version control of course:
$ ls ~/.vim
... some files
init.vim
vimrc
settings/
... other directories
$ ls ~/.vim/settings
colors.vim
ale.vim
... other files
" In vimrc
set autoindent " 'vim-specific' setting
runtime settings/fold_fillchars.vim
if has('nvim')
is exactly what you want to separate the two cases in your
scripts. For example, to load plugins only for Vim or only for Neovim, you can
put your optional plugins in ~/.vim/pack/vim_or_neovim/opt
and then use this
kind of snippets:
if has('nvim')
" Load Neovim specific plugins
packadd LanguageClient-neovim " This plugin is not Neovim specific anymore,
" just here for the example
else
" Load Vim specific plugins
packadd traces.vim
endif
Results
You can see a few of those principles applied on my current repo. Be warned that it is still a little bit messy, because tidying all the files and plugins is very low priority on my TODO list. And also because writing this post made me verify and learn new things about how to further smooth my truly cross platform setup.
This article is licensed under Creative Commons v4.0