1{ stdenv, vim, vimPlugins, vim_configurable, neovim, buildEnv, writeText, writeScriptBin
2, nix-prefetch-hg, nix-prefetch-git
3, fetchFromGitHub, runtimeShell
4}:
5
6/*
7
8USAGE EXAMPLE
9=============
10
11Install Vim like this eg using nixos option environment.systemPackages which will provide
12vim-with-plugins in PATH:
13
14 vim_configurable.customize {
15 name = "vim-with-plugins";
16
17 # add custom .vimrc lines like this:
18 vimrcConfig.customRC = ''
19 set hidden
20 '';
21
22 # store your plugins in Vim packages
23 vimrcConfig.packages.myVimPackage = with pkgs.vimPlugins; {
24 # loaded on launch
25 start = [ youcompleteme fugitive ];
26 # manually loadable by calling `:packadd $plugin-name`
27 opt = [ phpCompletion elm-vim ];
28 # To automatically load a plugin when opening a filetype, add vimrc lines like:
29 # autocmd FileType php :packadd phpCompletion
30 };
31
32 # plugins can also be managed by VAM
33 vimrcConfig.vam.knownPlugins = pkgs.vimPlugins; # optional
34 vimrcConfig.vam.pluginDictionaries = [
35 # load always
36 { name = "youcompleteme"; }
37 { names = ["youcompleteme" "foo"]; }
38
39 # only load when opening a .php file
40 { name = "phpCompletion"; ft_regex = "^php\$"; }
41 { name = "phpCompletion"; filename_regex = "^.php\$"; }
42
43 # provide plugin which can be loaded manually:
44 { name = "phpCompletion"; tag = "lazy"; }
45
46 # full documentation at github.com/MarcWeber/vim-addon-manager
47 ];
48
49 # there is a pathogen implementation as well, but its startup is slower and [VAM] has more feature
50 # vimrcConfig.pathogen.knownPlugins = vimPlugins; # optional
51 # vimrcConfig.pathogen.pluginNames = ["vim-addon-nix"];
52 };
53
54WHAT IS A VIM PLUGIN?
55=====================
56Typical plugin files:
57
58 plugin/P1.vim
59 autoload/P1.vim
60 ftplugin/xyz.vim
61 doc/plugin-documentation.txt (traditional documentation)
62 README(.md) (nowadays thanks to github)
63
64
65Vim offers the :h rtp setting which works for most plugins. Thus adding
66this to your .vimrc should make most plugins work:
67
68 set rtp+=~/.nix-profile/share/vim-plugins/youcompleteme
69 " or for p in ["youcompleteme"] | exec 'set rtp+=~/.nix-profile/share/vim-plugins/'.p | endfor
70
71which is what the [VAM]/pathogen solutions above basically do.
72
73Learn about about plugin Vim plugin mm managers at
74http://vim-wiki.mawercer.de/wiki/topic/vim%20plugin%20managment.html.
75
76The documentation can be accessed by Vim's :help command if it was tagged.
77See vimHelpTags sample code below.
78
79CONTRIBUTING AND CUSTOMIZING
80============================
81The example file pkgs/misc/vim-plugins/default.nix provides both:
82* manually mantained plugins
83* plugins created by VAM's nix#ExportPluginsForNix implementation
84
85I highly recommend to lookup vim plugin attribute names at the [vim-pi] project
86 which is a database containing all plugins from
87vim.org and quite a lot of found at github and similar sources. vim-pi's documented purpose
88is to associate vim.org script ids to human readable names so that dependencies
89can be describe easily.
90
91How to find a name?
92 * http://vam.mawercer.de/ or VAM's
93 * grep vim-pi
94 * use VAM's completion or :AddonsInfo command
95
96It might happen than a plugin is not known by vim-pi yet. We encourage you to
97contribute to vim-pi so that plugins can be updated automatically.
98
99
100CREATING DERVITATIONS AUTOMATICALLY BY PLUGIN NAME
101==================================================
102Most convenient is to use a ~/.vim-scripts file putting a plugin name into each line
103as documented by [VAM]'s README.md
104It is the same format you pass to vimrcConfig.vam.pluginDictionaries from the
105usage example above.
106
107Then create a temp vim file and insert:
108
109 let opts = {}
110 let opts.path_to_nixpkgs = '/etc/nixos/nixpkgs'
111 let opts.cache_file = '/tmp/export-vim-plugin-for-nix-cache-file'
112 let opts.plugin_dictionaries = map(readfile("vim-plugins"), 'eval(v:val)')
113 " add more files
114 " let opts.plugin_dictionaries += map(.. other file )
115 call nix#ExportPluginsForNix(opts)
116
117Then ":source %" it.
118
119nix#ExportPluginsForNix is provided by ./vim2nix
120
121A buffer will open containing the plugin derivation lines as well list
122fitting the vimrcConfig.vam.pluginDictionaries option.
123
124Thus the most simple usage would be:
125
126 vim_with_plugins =
127 let vim = vim_configurable;
128 inherit (vimUtil.override {inherit vim}) rtpPath addRtp buildVimPlugin vimHelpTags;
129 vimPlugins = [
130 # the derivation list from the buffer created by nix#ExportPluginsForNix
131 # don't set which will default to pkgs.vimPlugins
132 ];
133 in vim.customize {
134 name = "vim-with-plugins";
135
136 vimrcConfig.customRC = '' .. '';
137
138 vimrcConfig.vam.knownPlugins = vimPlugins;
139 vimrcConfig.vam.pluginDictionaries = [
140 # the plugin list form ~/.vim-scripts turned into nix format added to
141 # the buffer created by the nix#ExportPluginsForNix
142 ];
143 }
144
145vim_with_plugins can be installed like any other application within Nix.
146
147[VAM] https://github.com/MarcWeber/vim-addon-manager
148[vim-pi] https://bitbucket.org/vimcommunity/vim-pi
149*/
150
151
152let
153 inherit (stdenv) lib;
154
155 # make sure a plugin is a derivation and its dependencies are derivations. If
156 # plugin already is a derivation, this is a no-op. If it is a string, it is
157 # looked up in knownPlugins.
158 pluginToDrv = knownPlugins: plugin:
159 let
160 drv =
161 if builtins.isString plugin then
162 # make sure `pname` is set to that we are able to convert the derivation
163 # back to a string.
164 ( knownPlugins.${plugin} // { pname = plugin; })
165 else
166 plugin;
167 in
168 # make sure all the dependencies of the plugin are also derivations
169 drv // { dependencies = map (pluginToDrv knownPlugins) (drv.dependencies or []); };
170
171 # transitive closure of plugin dependencies (plugin needs to be a derivation)
172 transitiveClosure = plugin:
173 [ plugin ] ++ (
174 lib.unique (builtins.concatLists (map transitiveClosure plugin.dependencies or []))
175 );
176
177 findDependenciesRecursively = plugins: lib.concatMap transitiveClosure plugins;
178
179 vamDictToNames = x:
180 if builtins.isString x then [x]
181 else (lib.optional (x ? name) x.name)
182 ++ (x.names or []);
183
184 rtpPath = "share/vim-plugins";
185
186 vimrcFile = {
187 packages ? null,
188 vam ? null,
189 pathogen ? null,
190 plug ? null,
191 beforePlugins ? "",
192 customRC ? ""
193 }:
194
195 let
196 /* pathogen mostly can set &rtp at startup time. Its used very commonly.
197 */
198 pathogenImpl = lib.optionalString (pathogen != null)
199 (let
200 knownPlugins = pathogen.knownPlugins or vimPlugins;
201
202 plugins = findDependenciesRecursively (map (pluginToDrv knownPlugins) pathogen.pluginNames);
203
204 pluginsEnv = buildEnv {
205 name = "pathogen-plugin-env";
206 paths = map (x: "${x}/${rtpPath}") plugins;
207 };
208 in
209 ''
210 let &rtp.=(empty(&rtp)?"":',')."${vimPlugins.pathogen.rtp}"
211 execute pathogen#infect('${pluginsEnv}/{}')
212
213 filetype indent plugin on | syn on
214 '');
215
216 /* vim-plug is an extremely popular vim plugin manager.
217 */
218 plugImpl = lib.optionalString (plug != null)
219 (''
220 source ${vimPlugins.vim-plug.rtp}/plug.vim
221 call plug#begin('/dev/null')
222
223 '' + (lib.concatMapStringsSep "\n" (pkg: "Plug '${pkg.rtp}'") plug.plugins) + ''
224
225 call plug#end()
226 '');
227
228 /*
229 vim-addon-manager = VAM
230
231 * maps names to plugin location
232
233 * manipulates &rtp at startup time
234 or when Vim has been running for a while
235
236 * can activate plugins laziy (eg when loading a specific filetype)
237
238 * knows about vim plugin dependencies (addon-info.json files)
239
240 * still is minimalistic (only loads one file), the "check out" code it also
241 has only gets loaded when a plugin is requested which is not found on disk
242 yet
243
244 */
245 vamImpl = lib.optionalString (vam != null)
246 (let
247 knownPlugins = vam.knownPlugins or vimPlugins;
248
249 # plugins specified by the user
250 specifiedPlugins = map (pluginToDrv knownPlugins) (lib.concatMap vamDictToNames vam.pluginDictionaries);
251 # plugins with dependencies
252 plugins = findDependenciesRecursively specifiedPlugins;
253
254 # Convert scalars, lists, and attrs, to VimL equivalents
255 toVimL = x:
256 if builtins.isString x then "'${lib.replaceStrings [ "\n" "'" ] [ "\n\\ " "''" ] x}'"
257 else if builtins.isAttrs x && builtins ? out then toVimL x # a derivation
258 else if builtins.isAttrs x then "{${lib.concatStringsSep ", " (lib.mapAttrsToList (n: v: "${toVimL n}: ${toVimL v}") x)}}"
259 else if builtins.isList x then "[${lib.concatMapStringsSep ", " toVimL x}]"
260 else if builtins.isInt x || builtins.isFloat x then builtins.toString x
261 else if builtins.isBool x then (if x then "1" else "0")
262 else throw "turning ${lib.generators.toPretty {} x} into a VimL thing not implemented yet";
263
264 in assert builtins.hasAttr "vim-addon-manager" knownPlugins;
265 ''
266 filetype indent plugin on | syn on
267
268 let g:nix_plugin_locations = {}
269 ${lib.concatMapStrings (plugin: ''
270 let g:nix_plugin_locations['${plugin.pname}'] = "${plugin.rtp}"
271 '') plugins}
272 let g:nix_plugin_locations['vim-addon-manager'] = "${knownPlugins.vim-addon-manager.rtp}"
273
274 let g:vim_addon_manager = {}
275
276 if exists('g:nix_plugin_locations')
277 " nix managed config
278
279 " override default function making VAM aware of plugin locations:
280 fun! NixPluginLocation(name)
281 let path = get(g:nix_plugin_locations, a:name, "")
282 return path == "" ? vam#DefaultPluginDirFromName(a:name) : path
283 endfun
284 let g:vim_addon_manager.plugin_dir_by_name = 'NixPluginLocation'
285 " tell Vim about VAM:
286 let &rtp.=(empty(&rtp)?"":','). g:nix_plugin_locations['vim-addon-manager']
287 else
288 " standalone config
289
290 let &rtp.=(empty(&rtp)?"":',').c.plugin_root_dir.'/vim-addon-manager'
291 if !isdirectory(c.plugin_root_dir.'/vim-addon-manager/autoload')
292 " checkout VAM
293 execute '!git clone --depth=1 https://github.com/MarcWeber/vim-addon-manager '
294 \ shellescape(c.plugin_root_dir.'/vim-addon-manager', 1)
295 endif
296 endif
297
298 " tell vam which plugins to load, and when:
299 let l = []
300 ${lib.concatMapStrings (p: "call add(l, ${toVimL p})\n") vam.pluginDictionaries}
301 call vam#Scripts(l, {})
302 '');
303
304 nativeImpl = lib.optionalString (packages != null)
305 (let
306 link = (packageName: dir: pluginPath: "ln -sf ${pluginPath}/share/vim-plugins/* $out/pack/${packageName}/${dir}");
307 packageLinks = (packageName: {start ? [], opt ? []}:
308 let
309 # `nativeImpl` expects packages to be derivations, not strings (as
310 # opposed to older implementations that have to maintain backwards
311 # compatibility). Therefore we don't need to deal with "knownPlugins"
312 # and can simply pass `null`.
313 depsOfOptionalPlugins = lib.subtractLists opt (findDependenciesRecursively opt);
314 startWithDeps = findDependenciesRecursively start;
315 in
316 ["mkdir -p $out/pack/${packageName}/start"]
317 # To avoid confusion, even dependencies of optional plugins are added
318 # to `start` (except if they are explicitly listed as optional plugins).
319 ++ (builtins.map (link packageName "start") (lib.unique (startWithDeps ++ depsOfOptionalPlugins)))
320 ++ ["mkdir -p $out/pack/${packageName}/opt"]
321 ++ (builtins.map (link packageName "opt") opt)
322 );
323 packDir = (packages:
324 stdenv.mkDerivation {
325 name = "vim-pack-dir";
326 src = ./.;
327 installPhase = lib.concatStringsSep
328 "\n"
329 (lib.flatten (lib.mapAttrsToList packageLinks packages));
330 preferLocalBuild = true;
331 }
332 );
333 in
334 ''
335 set packpath^=${packDir packages}
336 set runtimepath^=${packDir packages}
337
338 filetype indent plugin on | syn on
339 '');
340
341 in writeText "vimrc" ''
342 " configuration generated by NIX
343 set nocompatible
344
345 ${beforePlugins}
346
347 ${vamImpl}
348 ${pathogenImpl}
349 ${plugImpl}
350 ${nativeImpl}
351
352 ${customRC}
353 '';
354
355in
356
357rec {
358 inherit vimrcFile;
359
360 # shell script with custom name passing [-u vimrc] [-U gvimrc] to vim
361 vimWithRC = {
362 vimExecutable,
363 gvimExecutable,
364 vimManPages,
365 wrapManual,
366 wrapGui,
367 name ? "vim",
368 vimrcFile ? null,
369 gvimrcFile ? null,
370 vimExecutableName,
371 gvimExecutableName,
372 }:
373 let
374 rcOption = o: file: stdenv.lib.optionalString (file != null) "-${o} ${file}";
375 vimWrapperScript = writeScriptBin vimExecutableName ''
376 #!${runtimeShell}
377 exec ${vimExecutable} ${rcOption "u" vimrcFile} ${rcOption "U" gvimrcFile} "$@"
378 '';
379 gvimWrapperScript = writeScriptBin gvimExecutableName ''
380 #!${stdenv.shell}
381 exec ${gvimExecutable} ${rcOption "u" vimrcFile} ${rcOption "U" gvimrcFile} "$@"
382 '';
383 in
384 buildEnv {
385 inherit name;
386 paths = [
387 vimWrapperScript
388 ] ++ lib.optional wrapGui gvimWrapperScript
389 ++ lib.optional wrapManual vimManPages
390 ;
391 };
392
393 # add a customize option to a vim derivation
394 makeCustomizable = vim: vim // {
395 customize = {
396 name,
397 vimrcConfig,
398 wrapManual ? true,
399 wrapGui ? false,
400 vimExecutableName ? name,
401 gvimExecutableName ? (lib.concatStrings [ "g" name ]),
402 }: vimWithRC {
403 vimExecutable = "${vim}/bin/vim";
404 gvimExecutable = "${vim}/bin/gvim";
405 inherit name wrapManual wrapGui vimExecutableName gvimExecutableName;
406 vimrcFile = vimrcFile vimrcConfig;
407 vimManPages = buildEnv {
408 name = "vim-doc";
409 paths = [ vim ];
410 pathsToLink = [ "/share/man" ];
411 };
412 };
413
414 override = f: makeCustomizable (vim.override f);
415 overrideAttrs = f: makeCustomizable (vim.overrideAttrs f);
416 };
417
418 pluginnames2Nix = {name, namefiles} : vim_configurable.customize {
419 inherit name;
420 vimrcConfig.vam.knownPlugins = vimPlugins;
421 vimrcConfig.vam.pluginDictionaries = ["vim2nix"];
422 vimrcConfig.customRC = ''
423 " Yes - this is impure and will create the cache file and checkout vim-pi
424 " into ~/.vim/vim-addons
425 let g:vim_addon_manager.plugin_root_dir = "/tmp/vim2nix-".$USER
426 if !isdirectory(g:vim_addon_manager.plugin_root_dir)
427 call mkdir(g:vim_addon_manager.plugin_root_dir)
428 else
429 echom repeat("=", 80)
430 echom "WARNING: reusing cache directory :".g:vim_addon_manager.plugin_root_dir
431 echom repeat("=", 80)
432 endif
433 let opts = {}
434 let opts.nix_prefetch_git = "${nix-prefetch-git}/bin/nix-prefetch-git"
435 let opts.nix_prefetch_hg = "${nix-prefetch-hg}/bin/nix-prefetch-hg"
436 let opts.cache_file = g:vim_addon_manager.plugin_root_dir.'/cache'
437 let opts.plugin_dictionaries = []
438 ${lib.concatMapStrings (file: "let opts.plugin_dictionaries += map(readfile(\"${file}\"), 'eval(v:val)')\n") namefiles }
439
440 " uncomment for debugging failures
441 " let opts.try_catch = 0
442
443 " add more files
444 " let opts.plugin_dictionaries += map(.. other file )
445 call nix#ExportPluginsForNix(opts)
446 '';
447 };
448
449 vim_with_vim2nix = vim_configurable.customize { name = "vim"; vimrcConfig.vam.pluginDictionaries = [ "vim-addon-vim2nix" ]; };
450
451 inherit (import ./build-vim-plugin.nix { inherit stdenv rtpPath vim; }) buildVimPlugin buildVimPluginFrom2Nix;
452
453 # used to figure out which python dependencies etc. neovim needs
454 requiredPlugins = {
455 packages ? {},
456 givenKnownPlugins ? null,
457 vam ? null,
458 pathogen ? null,
459 plug ? null, ...
460 }:
461 let
462 # This is probably overcomplicated, but I don't understand this well enough to know what's necessary.
463 knownPlugins = if givenKnownPlugins != null then givenKnownPlugins else
464 if vam != null && vam ? knownPlugins then vam.knownPlugins else
465 if pathogen != null && pathogen ? knownPlugins then pathogen.knownPlugins else
466 vimPlugins;
467 pathogenPlugins = findDependenciesRecursively (map (pluginToDrv knownPlugins) pathogen.pluginNames);
468 vamPlugins = findDependenciesRecursively (map (pluginToDrv knownPlugins) (lib.concatMap vamDictToNames vam.pluginDictionaries));
469 nonNativePlugins = (lib.optionals (pathogen != null) pathogenPlugins)
470 ++ (lib.optionals (vam != null) vamPlugins)
471 ++ (lib.optionals (plug != null) plug.plugins);
472 nativePluginsConfigs = lib.attrsets.attrValues packages;
473 nativePlugins = lib.concatMap ({start?[], opt?[], knownPlugins?vimPlugins}: start++opt) nativePluginsConfigs;
474 in
475 nativePlugins ++ nonNativePlugins;
476
477
478 # test cases:
479 test_vim_with_vim_nix_using_vam = vim_configurable.customize {
480 name = "vim-with-vim-addon-nix-using-vam";
481 vimrcConfig.vam.pluginDictionaries = [{name = "vim-nix"; }];
482 };
483
484 test_vim_with_vim_nix_using_pathogen = vim_configurable.customize {
485 name = "vim-with-vim-addon-nix-using-pathogen";
486 vimrcConfig.pathogen.pluginNames = [ "vim-nix" ];
487 };
488
489 test_vim_with_vim_nix_using_plug = vim_configurable.customize {
490 name = "vim-with-vim-addon-nix-using-plug";
491 vimrcConfig.plug.plugins = with vimPlugins; [ vim-nix ];
492 };
493
494 test_vim_with_vim_nix = vim_configurable.customize {
495 name = "vim-with-vim-addon-nix";
496 vimrcConfig.packages.myVimPackage.start = with vimPlugins; [ vim-nix ];
497 };
498
499 # only neovim makes use of `requiredPlugins`, test this here
500 test_nvim_with_vim_nix_using_pathogen = neovim.override {
501 configure.pathogen.pluginNames = [ "vim-nix" ];
502 };
503
504 # regression test for https://github.com/NixOS/nixpkgs/issues/53112
505 # The user may have specified their own plugins which may not be formatted
506 # exactly as the generated ones. In particular, they may not have the `pname`
507 # attribute.
508 test_vim_with_custom_plugin = vim_configurable.customize {
509 name = "vim_with_custom_plugin";
510 vimrcConfig.vam.knownPlugins =
511 vimPlugins // ({
512 vim-trailing-whitespace = buildVimPluginFrom2Nix {
513 name = "vim-trailing-whitespace";
514 src = fetchFromGitHub {
515 owner = "bronson";
516 repo = "vim-trailing-whitespace";
517 rev = "4c596548216b7c19971f8fc94e38ef1a2b55fee6";
518 sha256 = "0f1cpnp1nxb4i5hgymjn2yn3k1jwkqmlgw1g02sq270lavp2dzs9";
519 };
520 # make sure string dependencies are handled
521 dependencies = [ "vim-nix" ];
522 };
523 });
524 vimrcConfig.vam.pluginDictionaries = [ { names = [ "vim-trailing-whitespace" ]; } ];
525 };
526
527 # system remote plugin manifest should be generated, deoplete should be usable
528 # without the user having to do `UpdateRemotePlugins`. To test, launch neovim
529 # and do `:call deoplete#enable()`. It will print an error if the remote
530 # plugin is not registered.
531 test_nvim_with_remote_plugin = neovim.override {
532 configure.pathogen.pluginNames = with vimPlugins; [ deoplete-nvim ];
533 };
534}