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 customRC ? ""
192 }:
193
194 let
195 /* pathogen mostly can set &rtp at startup time. Its used very commonly.
196 */
197 pathogenImpl = lib.optionalString (pathogen != null)
198 (let
199 knownPlugins = pathogen.knownPlugins or vimPlugins;
200
201 plugins = findDependenciesRecursively (map (pluginToDrv knownPlugins) pathogen.pluginNames);
202
203 pluginsEnv = buildEnv {
204 name = "pathogen-plugin-env";
205 paths = map (x: "${x}/${rtpPath}") plugins;
206 };
207 in
208 ''
209 let &rtp.=(empty(&rtp)?"":',')."${vimPlugins.pathogen.rtp}"
210 execute pathogen#infect('${pluginsEnv}/{}')
211
212 filetype indent plugin on | syn on
213 '');
214
215 /* vim-plug is an extremely popular vim plugin manager.
216 */
217 plugImpl = lib.optionalString (plug != null)
218 (''
219 source ${vimPlugins.vim-plug.rtp}/plug.vim
220 call plug#begin('/dev/null')
221
222 '' + (lib.concatMapStringsSep "\n" (pkg: "Plug '${pkg.rtp}'") plug.plugins) + ''
223
224 call plug#end()
225 '');
226
227 /*
228 vim-addon-manager = VAM
229
230 * maps names to plugin location
231
232 * manipulates &rtp at startup time
233 or when Vim has been running for a while
234
235 * can activate plugins laziy (eg when loading a specific filetype)
236
237 * knows about vim plugin dependencies (addon-info.json files)
238
239 * still is minimalistic (only loads one file), the "check out" code it also
240 has only gets loaded when a plugin is requested which is not found on disk
241 yet
242
243 */
244 vamImpl = lib.optionalString (vam != null)
245 (let
246 knownPlugins = vam.knownPlugins or vimPlugins;
247
248 # plugins specified by the user
249 specifiedPlugins = map (pluginToDrv knownPlugins) (lib.concatMap vamDictToNames vam.pluginDictionaries);
250 # plugins with dependencies
251 plugins = findDependenciesRecursively specifiedPlugins;
252
253 # Vim almost reads JSON, so eventually JSON support should be added to Nix
254 # TODO: proper quoting
255 toNix = x:
256 if (builtins.isString x) then "'${x}'"
257 else if builtins.isAttrs x && builtins ? out then toNix x # a derivation
258 else if builtins.isAttrs x then "{${lib.concatStringsSep ", " (lib.mapAttrsToList (n: v: "${toNix n}: ${toNix v}") x)}}"
259 else if builtins.isList x then "[${lib.concatMapStringsSep ", " toNix x}]"
260 else throw "turning ${lib.generators.toPretty {} x} into a VimL thing not implemented yet";
261
262 in assert builtins.hasAttr "vim-addon-manager" knownPlugins;
263 ''
264 filetype indent plugin on | syn on
265
266 let g:nix_plugin_locations = {}
267 ${lib.concatMapStrings (plugin: ''
268 let g:nix_plugin_locations['${plugin.pname}'] = "${plugin.rtp}"
269 '') plugins}
270 let g:nix_plugin_locations['vim-addon-manager'] = "${knownPlugins.vim-addon-manager.rtp}"
271
272 let g:vim_addon_manager = {}
273
274 if exists('g:nix_plugin_locations')
275 " nix managed config
276
277 " override default function making VAM aware of plugin locations:
278 fun! NixPluginLocation(name)
279 let path = get(g:nix_plugin_locations, a:name, "")
280 return path == "" ? vam#DefaultPluginDirFromName(a:name) : path
281 endfun
282 let g:vim_addon_manager.plugin_dir_by_name = 'NixPluginLocation'
283 " tell Vim about VAM:
284 let &rtp.=(empty(&rtp)?"":','). g:nix_plugin_locations['vim-addon-manager']
285 else
286 " standalone config
287
288 let &rtp.=(empty(&rtp)?"":',').c.plugin_root_dir.'/vim-addon-manager'
289 if !isdirectory(c.plugin_root_dir.'/vim-addon-manager/autoload')
290 " checkout VAM
291 execute '!git clone --depth=1 https://github.com/MarcWeber/vim-addon-manager '
292 \ shellescape(c.plugin_root_dir.'/vim-addon-manager', 1)
293 endif
294 endif
295
296 " tell vam about which plugins to load when:
297 let l = []
298 ${lib.concatMapStrings (p: "call add(l, {'name': '${p.pname}'})\n") plugins}
299 call vam#Scripts(l, {})
300 '');
301
302 nativeImpl = lib.optionalString (packages != null)
303 (let
304 link = (packageName: dir: pluginPath: "ln -sf ${pluginPath}/share/vim-plugins/* $out/pack/${packageName}/${dir}");
305 packageLinks = (packageName: {start ? [], opt ? []}:
306 let
307 # `nativeImpl` expects packages to be derivations, not strings (as
308 # opposed to older implementations that have to maintain backwards
309 # compatibility). Therefore we don't need to deal with "knownPlugins"
310 # and can simply pass `null`.
311 depsOfOptionalPlugins = lib.subtractLists opt (findDependenciesRecursively opt);
312 startWithDeps = findDependenciesRecursively start;
313 in
314 ["mkdir -p $out/pack/${packageName}/start"]
315 # To avoid confusion, even dependencies of optional plugins are added
316 # to `start` (except if they are explicitly listed as optional plugins).
317 ++ (builtins.map (link packageName "start") (lib.unique (startWithDeps ++ depsOfOptionalPlugins)))
318 ++ ["mkdir -p $out/pack/${packageName}/opt"]
319 ++ (builtins.map (link packageName "opt") opt)
320 );
321 packDir = (packages:
322 stdenv.mkDerivation {
323 name = "vim-pack-dir";
324 src = ./.;
325 installPhase = lib.concatStringsSep
326 "\n"
327 (lib.flatten (lib.mapAttrsToList packageLinks packages));
328 preferLocalBuild = true;
329 }
330 );
331 in
332 ''
333 set packpath^=${packDir packages}
334 set runtimepath^=${packDir packages}
335
336 filetype indent plugin on | syn on
337 '');
338
339 in writeText "vimrc" ''
340 " configuration generated by NIX
341 set nocompatible
342
343 ${vamImpl}
344 ${pathogenImpl}
345 ${plugImpl}
346 ${nativeImpl}
347
348 ${customRC}
349 '';
350
351in
352
353rec {
354 inherit vimrcFile;
355
356 # shell script with custom name passing [-u vimrc] [-U gvimrc] to vim
357 vimWithRC = {
358 vimExecutable,
359 gvimExecutable,
360 vimManPages,
361 wrapManual,
362 wrapGui,
363 name ? "vim",
364 vimrcFile ? null,
365 gvimrcFile ? null,
366 vimExecutableName,
367 gvimExecutableName,
368 }:
369 let
370 rcOption = o: file: stdenv.lib.optionalString (file != null) "-${o} ${file}";
371 vimWrapperScript = writeScriptBin vimExecutableName ''
372 #!${runtimeShell}
373 exec ${vimExecutable} ${rcOption "u" vimrcFile} ${rcOption "U" gvimrcFile} "$@"
374 '';
375 gvimWrapperScript = writeScriptBin gvimExecutableName ''
376 #!${stdenv.shell}
377 exec ${gvimExecutable} ${rcOption "u" vimrcFile} ${rcOption "U" gvimrcFile} "$@"
378 '';
379 in
380 buildEnv {
381 inherit name;
382 paths = [
383 vimWrapperScript
384 ] ++ lib.optional wrapGui gvimWrapperScript
385 ++ lib.optional wrapManual vimManPages
386 ;
387 };
388
389 # add a customize option to a vim derivation
390 makeCustomizable = vim: vim // {
391 customize = {
392 name,
393 vimrcConfig,
394 wrapManual ? true,
395 wrapGui ? false,
396 vimExecutableName ? name,
397 gvimExecutableName ? (lib.concatStrings [ "g" name ]),
398 }: vimWithRC {
399 vimExecutable = "${vim}/bin/vim";
400 gvimExecutable = "${vim}/bin/gvim";
401 inherit name wrapManual wrapGui vimExecutableName gvimExecutableName;
402 vimrcFile = vimrcFile vimrcConfig;
403 vimManPages = buildEnv {
404 name = "vim-doc";
405 paths = [ vim ];
406 pathsToLink = [ "/share/man" ];
407 };
408 };
409
410 override = f: makeCustomizable (vim.override f);
411 overrideAttrs = f: makeCustomizable (vim.overrideAttrs f);
412 };
413
414 pluginnames2Nix = {name, namefiles} : vim_configurable.customize {
415 inherit name;
416 vimrcConfig.vam.knownPlugins = vimPlugins;
417 vimrcConfig.vam.pluginDictionaries = ["vim2nix"];
418 vimrcConfig.customRC = ''
419 " Yes - this is impure and will create the cache file and checkout vim-pi
420 " into ~/.vim/vim-addons
421 let g:vim_addon_manager.plugin_root_dir = "/tmp/vim2nix-".$USER
422 if !isdirectory(g:vim_addon_manager.plugin_root_dir)
423 call mkdir(g:vim_addon_manager.plugin_root_dir)
424 else
425 echom repeat("=", 80)
426 echom "WARNING: reusing cache directory :".g:vim_addon_manager.plugin_root_dir
427 echom repeat("=", 80)
428 endif
429 let opts = {}
430 let opts.nix_prefetch_git = "${nix-prefetch-git}/bin/nix-prefetch-git"
431 let opts.nix_prefetch_hg = "${nix-prefetch-hg}/bin/nix-prefetch-hg"
432 let opts.cache_file = g:vim_addon_manager.plugin_root_dir.'/cache'
433 let opts.plugin_dictionaries = []
434 ${lib.concatMapStrings (file: "let opts.plugin_dictionaries += map(readfile(\"${file}\"), 'eval(v:val)')\n") namefiles }
435
436 " uncomment for debugging failures
437 " let opts.try_catch = 0
438
439 " add more files
440 " let opts.plugin_dictionaries += map(.. other file )
441 call nix#ExportPluginsForNix(opts)
442 '';
443 };
444
445 vim_with_vim2nix = vim_configurable.customize { name = "vim"; vimrcConfig.vam.pluginDictionaries = [ "vim-addon-vim2nix" ]; };
446
447 inherit (import ./build-vim-plugin.nix { inherit stdenv rtpPath vim; }) buildVimPlugin buildVimPluginFrom2Nix;
448
449 # used to figure out which python dependencies etc. neovim needs
450 requiredPlugins = {
451 packages ? {},
452 givenKnownPlugins ? null,
453 vam ? null,
454 pathogen ? null,
455 plug ? null, ...
456 }:
457 let
458 # This is probably overcomplicated, but I don't understand this well enough to know what's necessary.
459 knownPlugins = if givenKnownPlugins != null then givenKnownPlugins else
460 if vam != null && vam ? knownPlugins then vam.knownPlugins else
461 if pathogen != null && pathogen ? knownPlugins then pathogen.knownPlugins else
462 vimPlugins;
463 pathogenPlugins = findDependenciesRecursively (map (pluginToDrv knownPlugins) pathogen.pluginNames);
464 vamPlugins = findDependenciesRecursively (map (pluginToDrv knownPlugins) (lib.concatMap vamDictToNames vam.pluginDictionaries));
465 nonNativePlugins = (lib.optionals (pathogen != null) pathogenPlugins)
466 ++ (lib.optionals (vam != null) vamPlugins)
467 ++ (lib.optionals (plug != null) plug.plugins);
468 nativePluginsConfigs = lib.attrsets.attrValues packages;
469 nativePlugins = lib.concatMap ({start?[], opt?[], knownPlugins?vimPlugins}: start++opt) nativePluginsConfigs;
470 in
471 nativePlugins ++ nonNativePlugins;
472
473
474 # test cases:
475 test_vim_with_vim_nix_using_vam = vim_configurable.customize {
476 name = "vim-with-vim-addon-nix-using-vam";
477 vimrcConfig.vam.pluginDictionaries = [{name = "vim-nix"; }];
478 };
479
480 test_vim_with_vim_nix_using_pathogen = vim_configurable.customize {
481 name = "vim-with-vim-addon-nix-using-pathogen";
482 vimrcConfig.pathogen.pluginNames = [ "vim-nix" ];
483 };
484
485 test_vim_with_vim_nix_using_plug = vim_configurable.customize {
486 name = "vim-with-vim-addon-nix-using-plug";
487 vimrcConfig.plug.plugins = with vimPlugins; [ vim-nix ];
488 };
489
490 test_vim_with_vim_nix = vim_configurable.customize {
491 name = "vim-with-vim-addon-nix";
492 vimrcConfig.packages.myVimPackage.start = with vimPlugins; [ vim-nix ];
493 };
494
495 # only neovim makes use of `requiredPlugins`, test this here
496 test_nvim_with_vim_nix_using_pathogen = neovim.override {
497 configure.pathogen.pluginNames = [ "vim-nix" ];
498 };
499
500 # regression test for https://github.com/NixOS/nixpkgs/issues/53112
501 # The user may have specified their own plugins which may not be formatted
502 # exactly as the generated ones. In particular, they may not have the `pname`
503 # attribute.
504 test_vim_with_custom_plugin = vim_configurable.customize {
505 name = "vim_with_custom_plugin";
506 vimrcConfig.vam.knownPlugins =
507 vimPlugins // ({
508 vim-trailing-whitespace = buildVimPluginFrom2Nix {
509 name = "vim-trailing-whitespace";
510 src = fetchFromGitHub {
511 owner = "bronson";
512 repo = "vim-trailing-whitespace";
513 rev = "4c596548216b7c19971f8fc94e38ef1a2b55fee6";
514 sha256 = "0f1cpnp1nxb4i5hgymjn2yn3k1jwkqmlgw1g02sq270lavp2dzs9";
515 };
516 # make sure string dependencies are handled
517 dependencies = [ "vim-nix" ];
518 };
519 });
520 vimrcConfig.vam.pluginDictionaries = [ { names = [ "vim-trailing-whitespace" ]; } ];
521 };
522
523 # system remote plugin manifest should be generated, deoplete should be usable
524 # without the user having to do `UpdateRemotePlugins`. To test, launch neovim
525 # and do `:call deoplete#enable()`. It will print an error if the remote
526 # plugin is not registered.
527 test_nvim_with_remote_plugin = neovim.override {
528 configure.pathogen.pluginNames = with vimPlugins; [ deoplete-nvim ];
529 };
530}