forked from nvim-mini/mini.nvim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeps.lua
More file actions
1638 lines (1443 loc) · 63 KB
/
deps.lua
File metadata and controls
1638 lines (1443 loc) · 63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--- *mini.deps* Plugin manager
---
--- MIT License Copyright (c) 2024 Evgeni Chasnovski
--- Features:
---
--- - Manage plugins utilizing Git and built-in |packages| with these actions:
--- - Add plugin to current session, download if absent. See |MiniDeps.add()|.
--- - Update with/without confirm, with/without parallel download of new data.
--- See |MiniDeps.update()|.
--- - Delete unused plugins with/without confirm. See |MiniDeps.clean()|.
--- - Get / set / save / load snapshot. See `MiniDeps.snap_*()` functions.
---
--- All main actions are available both as Lua functions and user commands
--- (see |MiniDeps-commands|).
---
--- - Minimal yet flexible plugin |MiniDeps-plugin-specification|:
--- - Plugin source.
--- - Name of target plugin directory.
--- - Checkout target: branch, commit, tag, etc.
--- - Monitor branch to track updates without checking out.
--- - Dependencies to be set up prior to the target plugin.
--- - Hooks to call before/after plugin is created/changed.
---
--- - Helpers implementing two-stage startup: |MiniDeps.now()| and |MiniDeps.later()|.
--- See |MiniDeps-overview| for how to implement basic lazy loading with them.
---
--- What it doesn't do:
---
--- - Manage plugins which are developed without Git. The suggested approach is
--- to create a separate package (see |packages|).
---
--- - Provide ways to completely remove or update plugin's functionality in
--- current session. Although this is partially doable, it can not be done
--- in full (yet) because plugins can have untraceable side effects
--- (autocmmands, mappings, etc.).
--- The suggested approach is to restart Nvim.
---
--- Sources with more details:
--- - |MiniDeps-overview|
--- - |MiniDeps-plugin-specification|
--- - |MiniDeps-commands|
---
--- # Dependencies ~
---
--- For most of its functionality this plugin relies on `git` CLI tool.
--- See https://git-scm.com/ for more information about how to install it.
--- Actual knowledge of Git is not required but helpful.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.deps').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniDeps`
--- which you can use for scripting or manually (with `:lua MiniDeps.*`).
---
--- See |MiniDeps.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minideps_config` which should have same structure as
--- `MiniDeps.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - [folke/lazy.nvim](https://github.com/folke/lazy.nvim):
--- - More feature-rich and complex.
--- - Uses table specification with dedicated functions to add plugins,
--- while this module uses direct function call approach
--- (calling |MiniDeps.add()| ensures that plugin is usable).
--- - Uses version tags by default, while this module is more designed towards
--- tracking branches. Using tags is possible too (see |MiniDeps-overview|).
---
--- - [savq/paq-nvim](https://github.com/savq/paq-nvim):
--- - Overall less feature-rich than this module (by design).
--- - Uses array of plugin specifications inside `setup()` call to define which
--- plugins should be installed. Requires separate `:PaqInstall` call to
--- actually install them. This module ensures installation on first load.
---
--- - [junegunn/vim-plug](https://github.com/junegunn/vim-plug):
--- - Written in Vimscript, while this module is in Lua.
--- - Similar approach to defining and installing plugins as 'savq/paq-nvim'.
--- - Has basic lazy-loading built-in, while this module does not (by design).
---
--- # Highlight groups ~
---
--- Highlight groups are used inside confirmation buffers after
--- default |MiniDeps.update()| and |MiniDeps.clean()|.
---
--- - `MiniDepsChangeAdded` - added change (commit) during update.
--- - `MiniDepsChangeRemoved` - removed change (commit) during update.
--- - `MiniDepsHint` - various hints.
--- - `MiniDepsInfo` - various information.
--- - `MiniDepsMsgBreaking` - message for (conventional commit) breaking change.
--- - `MiniDepsPlaceholder` - placeholder when there is no valuable information.
--- - `MiniDepsTitle` - various titles.
--- - `MiniDepsTitleError` - title when plugin had errors during update.
--- - `MiniDepsTitleSame` - title when plugin has no changes to update.
--- - `MiniDepsTitleUpdate` - title when plugin has changes to update.
---
--- To change any highlight group, set it directly with |nvim_set_hl()|.
---@tag MiniDeps
--- # Directory structure ~
---
--- This module uses built-in |packages| to make plugins usable in current session.
--- It works with "pack/deps" package inside `config.path.package` directory.
---
--- By default "opt" subdirectory is used to install optional plugins which are
--- loaded on demand with |MiniDeps.add()|.
--- Non-optional plugins in "start" subdirectory are supported but only if moved
--- there manually after initial install. Use it if you know what you are doing.
---
--- # Add plugin ~
---
--- Use |MiniDeps.add()| to add plugin to current session. Supply plugin's URL
--- source as a string or |MiniDeps-plugin-specification| in general. If plugin is
--- not present in "pack/deps" package, it will be created (a.k.a. installed)
--- before processing anything else.
---
--- The recommended way of adding a plugin is by calling |MiniDeps.add()| in the
--- |init.lua| file (make sure |MiniDeps.setup()| is called prior): >lua
---
--- local add = MiniDeps.add
---
--- -- Add to current session (install if absent)
--- add({
--- source = 'neovim/nvim-lspconfig',
--- -- Supply dependencies near target plugin
--- depends = { 'williamboman/mason.nvim' },
--- })
---
--- add({
--- source = 'nvim-treesitter/nvim-treesitter',
--- -- Use 'master' while monitoring updates in 'main'
--- checkout = 'master',
--- monitor = 'main',
--- -- Perform action after every checkout
--- hooks = { post_checkout = function() vim.cmd('TSUpdate') end },
--- })
--- -- Possible to immediately execute code which depends on the added plugin
--- require('nvim-treesitter.configs').setup({
--- ensure_installed = { 'lua', 'vimdoc' },
--- highlight = { enable = true },
--- })
--- <
--- NOTE:
--- - To increase performance, `add()` only ensures presence on disk and
--- nothing else. In particular, it doesn't ensure `opts.checkout` state.
--- Update or modify plugin state explicitly (see later sections).
---
--- # Lazy loading ~
---
--- Any lazy-loading is assumed to be done manually by calling |MiniDeps.add()|
--- at appropriate time. This module provides helpers implementing special safe
--- two-stage loading:
--- - |MiniDeps.now()| safely executes code immediately. Use it to load plugins
--- with UI necessary to make initial screen draw.
--- - |MiniDeps.later()| schedules code to be safely executed later, preserving
--- order. Use it (with caution) for everything else which doesn't need
--- precisely timed effect, as it will be executed some time soon on one of
--- the next event loops. >lua
---
--- local now, later = MiniDeps.now, MiniDeps.later
---
--- -- Safely execute immediately
--- now(function() vim.cmd('colorscheme miniwinter') end)
--- now(function() require('mini.statusline').setup() end)
---
--- -- Safely execute later
--- later(function() require('mini.pick').setup() end)
--- <
--- # Update ~
---
--- To update plugins from current session with new data from their sources,
--- use |:DepsUpdate|. This will download updates (utilizing multiple cores) and
--- show confirmation buffer. Follow instructions at its top to finish an update.
---
--- NOTE: This updates plugins on disk which most likely won't affect current
--- session. Restart Nvim to have them properly loaded.
---
--- # Modify ~
---
--- To change plugin's specification (like set different `checkout`, etc.):
--- - Update corresponding |MiniDeps.add()| call.
--- - Run `:DepsUpdateOffline <plugin_name>`.
--- - Review changes and confirm.
--- - Restart Nvim.
---
--- NOTE: if `add()` prior used a single source string, make sure to convert
--- its argument to `{ source = '<previous_argument>', checkout = '<state>'}`
---
--- # Snapshots ~
---
--- Use |:DepsSnapSave| to save state of all plugins from current session into
--- a snapshot file (see `config.path.snapshot`).
---
--- Use |:DepsSnapLoad| to load snapshot. This will change (without confirmation)
--- state on disk. Plugins present in both snapshot file and current session
--- will be affected. Restart Nvim to see the effect.
---
--- NOTE: loading snapshot does not change plugin's specification defined inside
--- |MiniDeps.add()| call. This means that next update might change plugin's state.
--- To make it permanent, freeze plugin in target state manually.
---
--- # Freeze ~
---
--- Modify plugin's specification to have `checkout` pointing to a static
--- target: tag, state (commit hash), or 'HEAD' (to freeze in current state).
---
--- Frozen plugins will not receive updates. You can monitor any new changes from
--- its source by "subscribing" to `monitor` branch which will be shown inside
--- confirmation buffer after |:DepsUpdate|.
---
--- Example: use `checkout = 'v0.10.0'` to freeze plugin at tag "v0.10.0" while
--- monitoring new versions in the log from `monitor` (usually default) branch.
---
--- # Rollback ~
---
--- To roll back after an unfortunate update:
--- - Get identifier of latest working state:
--- - Use |:DepsShowLog| to see update log, look for plugin's name, and copy
--- identifier listed as "State before:".
--- - See previously saved snapshot file for plugin's name and copy
--- identifier next to it.
--- - Freeze plugin at that state while monitoring appropriate branch.
--- Revert to previous shape of |MiniDeps.add()| call to resume updating.
---
--- # Remove ~
---
--- - Make sure that target plugin is not registered in current session.
--- Usually it means removing corresponding |MiniDeps.add()| call.
--- - Run |:DepsClean|. This will show confirmation buffer with a list of plugins to
--- be deleted from disk. Follow instructions at its top to finish cleaning.
---
--- Alternatively, manually delete plugin's directory from "pack/deps" package.
---@tag MiniDeps-overview
--- Each plugin dependency is managed based on its specification (a.k.a. "spec").
--- See |MiniDeps-overview| for some examples.
---
--- Specification can be a single string which is inferred as:
--- - Plugin <name> if it doesn't contain "/".
--- - Plugin <source> otherwise.
---
--- Primarily, specification is a table with the following fields:
---
--- - <source> `(string|nil)` - field with URI of plugin source used during creation
--- or update. Can be anything allowed by `git clone`.
--- Default: `nil` to rely on source set up during install.
--- Notes:
--- - It is required for creating plugin, but can be omitted afterwards.
--- - As the most common case, URI of the format "user/repo" (if it contains
--- valid characters) is transformed into "https://github.com/user/repo".
---
--- - <name> `(string|nil)` - directory basename of where to put plugin source.
--- It is put in "pack/deps/opt" subdirectory of `config.path.package`.
--- Default: basename of <source> if it is present, otherwise should be
--- provided explicitly.
---
--- - <checkout> `(string|nil)` - checkout target used to set state during update.
--- Can be anything supported by `git checkout` - branch, commit, tag, etc.
--- Default: `nil` for default branch (usually "main" or "master").
---
--- - <monitor> `(string|nil)` - monitor branch used to track new changes from
--- different target than `checkout`. Should be a name of present Git branch.
--- Default: `nil` for default branch (usually "main" or "master").
---
--- - <depends> `(table|nil)` - array of plugin specifications (strings or tables)
--- to be added prior to the target.
--- Default: `nil` for no dependencies.
---
--- - <hooks> `(table|nil)` - table with callable hooks to call on certain events.
--- Possible hook names:
--- - <pre_install> - before creating plugin directory.
--- - <post_install> - after creating plugin directory (before |:packadd|).
--- - <pre_checkout> - before making change in existing plugin.
--- - <post_checkout> - after making change in existing plugin.
--- Each hook is executed with the following table as an argument:
--- - <path> (`string`) - absolute path to plugin's directory
--- (might not yet exist on disk).
--- - <source> (`string`) - resolved <source> from spec.
--- - <name> (`string`) - resolved <name> from spec.
--- Default: `nil` for no hooks.
---@tag MiniDeps-plugin-specification
--- Note: Most commands have a Lua function alternative which they rely on.
--- Like |:DepsAdd| uses |MiniDeps.add()|, etc.
---
--- # :DepsAdd ~
---
--- *:DepsAdd* with `user/repo` argument makes plugin https://github.com/user/repo
--- available in the current session (also creates it, if it is not present).
--- `:DepsAdd name` adds already installed plugin `name` to current session.
--- Accepts only single string compatible with |MiniDeps-plugin-specification|.
--- To add plugin in every session, put |MiniDeps.add()| in |init.lua|.
---
--- # :DepsUpdate ~
---
--- *:DepsUpdate* synchronizes plugins with their session specifications and
--- updates them with new changes from sources. It shows confirmation buffer in
--- a separate |tabpage| with information about an upcoming update to review
--- and (selectively) apply. See |MiniDeps.update()| for more info.
---
--- `:DepsUpdate name` updates plugin `name`. Any number of names is allowed.
---
--- `:DepsUpdate!` and `:DepsUpdate! name` update without confirmation.
--- You can see what was done in the log file afterwards (|:DepsShowLog|).
---
--- # :DepsUpdateOffline ~
---
--- *:DepsUpdateOffline* is same as |:DepsUpdate| but doesn't download new updates
--- from sources. Useful to only synchronize plugin specification in code and
--- on disk without unnecessary downloads.
---
--- # :DepsShowLog ~
---
--- *:DepsShowLog* opens log file to review.
---
--- # :DepsClean ~
---
--- *:DepsClean* deletes plugins from disk not added to current session. It shows
--- confirmation buffer in a separate |tabpage| with information about an upcoming
--- deletes to review and (selectively) apply. See |MiniDeps.clean()| for more info.
---
--- `:DepsClean!` deletes plugins without confirmation.
---
--- # :DepsSnapSave ~
---
--- *:DepsSnapSave* creates snapshot file in default location (see |MiniDeps.config|).
--- `:DepsSnapSave path` creates snapshot file at `path`.
---
--- # :DepsSnapLoad ~
---
--- *:DepsSnapLoad* loads snapshot file from default location (see |MiniDeps.config|).
--- `:DepsSnapLoad path` loads snapshot file at `path`.
---@tag MiniDeps-commands
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
---@diagnostic disable:undefined-doc-name
---@diagnostic disable:luadoc-miss-type-name
-- Module definition ==========================================================
local MiniDeps = {}
local H = {}
--- Module setup
---
--- Calling this function creates user commands described in |MiniDeps-commands|.
---
---@param config table|nil Module config table. See |MiniDeps.config|.
---
---@usage >lua
--- require('mini.deps').setup() -- use default config
--- -- OR
--- require('mini.deps').setup({}) -- replace {} with your config table
--- <
MiniDeps.setup = function(config)
-- Export module
_G.MiniDeps = MiniDeps
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
-- Create default highlighting
H.create_default_hl()
-- Create user commands
H.create_user_commands()
end
--stylua: ignore
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Job ~
---
--- `config.job` defines how CLI jobs are run.
---
--- `job.n_threads` is a maximum number of parallel jobs used when needed.
--- Default: 80% of all available.
---
--- `job.timeout` is a duration (in ms) from job start until it is forced to stop.
--- Default: 30000.
---
--- # Paths ~
---
--- `config.path` defines main paths used in this module.
---
--- `path.package` is a string with path inside which "pack/deps" package is stored
--- (see |MiniDeps-overview|).
--- Default: "site" subdirectory of "data" standard path (see |stdpath()|).
---
--- `path.snapshot` is a string with default path for snapshot.
--- See |:DepsSnapSave| and |:DepsSnapLoad|.
--- Default: "mini-deps-snap" file in "config" standard path (see |stdpath()|).
---
--- `path.log` is a string with path containing log of operations done by module.
--- In particular, it contains all changes done after making an update.
--- Default: "mini-deps.log" file in "log" standard path (see |stdpath()|).
---
--- # Silent ~
---
--- `config.silent` is a boolean controlling whether to suppress non-error feedback.
--- Default: `false`.
MiniDeps.config = {
-- Parameters of CLI jobs
job = {
-- Number of parallel threads to use. Default: 80% of all available.
n_threads = nil,
-- Timeout (in ms) for each job before force quit
timeout = 30000,
},
-- Paths describing where to store data
path = {
-- Directory for built-in package.
-- All plugins are actually stored in 'pack/deps' subdirectory.
package = vim.fn.stdpath('data') .. '/site',
-- Default file path for a snapshot
snapshot = vim.fn.stdpath('config') .. '/mini-deps-snap',
-- Log file
--minidoc_replace_start log = vim.fn.stdpath('log') .. '/mini-deps.log'
log = vim.fn.stdpath('log') .. '/mini-deps.log',
--minidoc_replace_end
},
-- Whether to disable showing non-error feedback
silent = false,
}
--minidoc_afterlines_end
--- Add plugin to current session
---
--- - Process specification by expanding dependencies into single spec array.
--- - Ensure plugin is present on disk along with its dependencies by installing
--- (in parallel) absent ones:
--- - Execute `opts.hooks.pre_install`.
--- - Use `git clone` to clone plugin from its source URI into "pack/deps/opt".
--- - Set state according to `opts.checkout`.
--- - Execute `opts.hooks.post_install`.
--- - Register spec(s) in current session.
--- - Make sure plugin(s) can be used in current session (see |:packadd|).
--- - If not during startup and is needed, source all "after/plugin/" scripts.
---
--- Notes:
--- - Presence of plugin is checked by its name which is the same as the name
--- of its directory inside "pack/deps" package (see |MiniDeps-overview|).
--- - To increase performance, this function only ensures presence on disk and
--- nothing else. In particular, it doesn't ensure `opts.checkout` state.
--- Use |MiniDeps.update()| or |:DepsUpdateOffline| explicitly.
--- - Adding plugin several times updates its session specs.
---
---@param spec table|string Plugin specification. See |MiniDeps-plugin-specification|.
---@param opts table|nil Options. Possible fields:
--- - <bang> `(boolean)` - whether to use `:packadd!` instead of plain |:packadd|.
MiniDeps.add = function(spec, opts)
opts = opts or {}
if type(opts) ~= 'table' then H.error('`opts` should be table.') end
if opts.source or opts.name or opts.checkout then H.error('`add()` accepts only single spec.') end
-- Normalize
local plugs = {}
H.expand_spec(plugs, spec)
-- Process
local plugs_to_install = {}
for i, p in ipairs(plugs) do
local path, is_present = H.get_plugin_path(p.name)
p.path = path
if not is_present then table.insert(plugs_to_install, vim.deepcopy(p)) end
end
-- Install
if #plugs_to_install > 0 then
H.ensure_git_exec()
for _, p in ipairs(plugs_to_install) do
p.job = H.cli_new_job({}, vim.fn.getcwd())
end
H.notify(string.format('Installing `%s`', plugs[#plugs].name))
H.plugs_exec_hooks(plugs_to_install, 'pre_install')
H.plugs_install(plugs_to_install)
H.plugs_exec_hooks(plugs_to_install, 'post_install')
end
-- Add plugins to current session
local cmd = 'packadd' .. (opts.bang and '!' or '') .. ' '
for _, p in ipairs(plugs) do
-- Register in 'mini.deps' session
table.insert(H.session, p)
-- Add to 'runtimepath'
vim.cmd(cmd .. p.name)
end
-- Execute 'after/' scripts if not during startup (when they will be sourced
-- automatically), as `:packadd` only sources plain 'plugin/' files.
-- See https://github.com/vim/vim/issues/1994.
-- Deliberately do so after executing all currently known 'plugin/' files.
local should_load_after_dir = vim.v.vim_did_enter == 1 and not opts.bang and vim.o.loadplugins
if not should_load_after_dir then return end
for _, p in ipairs(plugs) do
-- NOTE: This sources first lua and then vim, not how it is done during
-- startup (`:h loadplugins`) for speed (one `glob()` instead of two).
local after_paths = vim.fn.glob(p.path .. '/after/plugin/**/*.{vim,lua}', false, true)
vim.tbl_map(H.source, after_paths)
end
end
--- Update plugins
---
--- - Synchronize specs with state of plugins on disk (set `source`, etc.).
--- - Infer data before downloading updates.
--- - If not offline, download updates (in parallel).
--- - Infer data after downloading updates.
--- - If update is forced, apply all changes immediately while updating log
--- file (at `config.path.log`; use |:DepsShowLog| to review).
--- Otherwise show confirmation buffer with instructions on how to proceed.
---
---@param names table|nil Array of plugin names to update.
--- Default: all plugins from current session (see |MiniDeps.get_session()|).
---@param opts table|nil Options. Possible fields:
--- - <force> `(boolean)` - whether to force update without confirmation.
--- Default: `false`.
--- - <offline> `(boolean)` - whether to skip downloading updates from sources.
--- Default: `false`.
MiniDeps.update = function(names, opts)
opts = vim.tbl_deep_extend('force', { force = false, offline = false }, opts or {})
-- Compute array of plugin data to be reused in update. Each contains a CLI
-- job "assigned" to plugin's path which stops execution after first error.
local plugs = H.plugs_from_names(names)
if #plugs == 0 then return H.notify('Nothing to update') end
-- Prepare repositories and specifications
H.ensure_git_exec()
H.plugs_ensure_origin_source(plugs)
-- Preprocess before downloading
H.plugs_infer_head(plugs)
H.plugs_ensure_target_refs(plugs)
H.plugs_infer_commit(plugs, 'monitor', 'monitor_from')
-- Download data if asked
if not opts.offline then H.plugs_download_updates(plugs) end
-- Process data for update
H.plugs_infer_commit(plugs, 'checkout', 'checkout_to')
H.plugs_infer_commit(plugs, 'monitor', 'monitor_to')
H.plugs_infer_log(plugs, 'head', 'checkout_to', 'checkout_log')
H.plugs_infer_log(plugs, 'monitor_from', 'monitor_to', 'monitor_log')
-- Checkout if asked (before feedback to include possible checkout errors)
if opts.force then H.plugs_checkout(plugs) end
-- Make feedback
local lines = H.update_compute_feedback_lines(plugs)
local feedback = opts.force and H.update_feedback_log or H.update_feedback_confirm
feedback(lines)
-- Show job warnings and errors
H.plugs_show_job_notifications(plugs, 'update')
end
--- Clean plugins
---
--- - Compute absent plugins: not registered in current session
--- (see |MiniDeps.get_session()|) but present on disk in dedicated "pack/deps"
--- package (inside `config.path.package`).
--- - If cleaning is forced, delete all absent plugins from disk.
--- Otherwise show confirmation buffer with instructions on how to proceed.
---
---@param opts table|nil Options. Possible fields:
--- - <force> `(boolean)` - whether to force delete without confirmation.
--- Default: `false`.
MiniDeps.clean = function(opts)
opts = vim.tbl_deep_extend('force', { force = false }, opts or {})
-- Compute path candidates to delete
local is_in_session = {}
for _, s in ipairs(MiniDeps.get_session()) do
is_in_session[s.path] = true
end
local is_absent_plugin = function(x) return vim.fn.isdirectory(x) == 1 and not is_in_session[x] end
local absent_paths = vim.tbl_filter(is_absent_plugin, H.get_all_plugin_paths())
-- Clean
if #absent_paths == 0 then return H.notify('Nothing to clean') end
local clean_fun = opts.force and H.clean_delete or H.clean_confirm
clean_fun(absent_paths)
end
--- Compute snapshot
---
---@return table A snapshot table: plugin names as keys and state as values.
--- All plugins in current session are processed.
MiniDeps.snap_get = function()
local plugs = H.plugs_from_names()
H.ensure_git_exec()
H.plugs_infer_head(plugs)
H.plugs_show_job_notifications(plugs, 'computing snapshot')
local snap = {}
for _, p in ipairs(plugs) do
if p.head ~= '' then snap[p.name] = p.head end
end
return snap
end
--- Apply snapshot
---
--- Notes:
--- - Checking out states from snapshot does not update session plugin spec
--- (`checkout` field in particular). Among others, it means that next call
--- to |MiniDeps.update()| might override the result of this function.
--- To make changes permanent, set `checkout` spec field to state from snapshot.
---
---@param snap table A snapshot table: plugin names as keys and state as values.
--- Only plugins in current session are processed.
MiniDeps.snap_set = function(snap)
if type(snap) ~= 'table' then H.error('Snapshot should be a table.') end
-- Construct current session plugin data with `checkout` from snapshot
for k, v in pairs(snap) do
if not (type(k) == 'string' and type(v) == 'string') then snap[k] = nil end
end
local plugs = H.plugs_from_names(vim.tbl_keys(snap))
for _, p in ipairs(plugs) do
p.checkout = snap[p.name]
end
-- Checkout
H.ensure_git_exec()
H.plugs_checkout(plugs)
H.plugs_show_job_notifications(plugs, 'applying snapshot')
end
--- Save snapshot
---
---@param path string|nil A valid path on disk where to write snapshot computed
--- with |MiniDeps.snap_get()|.
--- Default: `config.path.snapshot`.
MiniDeps.snap_save = function(path)
path = path or H.full_path(H.get_config().path.snapshot)
if type(path) ~= 'string' then H.error('`path` should be string.') end
-- Compute snapshot
local snap = MiniDeps.snap_get()
-- Write snapshot
local lines = vim.split(vim.inspect(snap), '\n')
lines[1] = 'return ' .. lines[1]
vim.fn.mkdir(vim.fn.fnamemodify(path, ':h'), 'p')
vim.fn.writefile(lines, path)
H.notify('Created snapshot at ' .. vim.inspect(path))
end
--- Load snapshot file
---
--- Notes from |MiniDeps.snap_set()| also apply here.
---
---@param path string|nil A valid path on disk from where to read snapshot.
--- Default: `config.path.snapshot`.
MiniDeps.snap_load = function(path)
path = path or H.full_path(H.get_config().path.snapshot)
if vim.fn.filereadable(path) ~= 1 then H.error('`path` should be path to a readable file.') end
local ok, snap = pcall(dofile, H.full_path(path))
if not (ok and type(snap) == 'table') then H.error('`path` is not a path to proper snapshot.') end
MiniDeps.snap_set(snap)
end
--- Get session
---
--- Plugin is registered in current session if it either:
--- - Was added with |MiniDeps.add()| (preserving order of calls).
--- - Is a "start" plugin and present in 'runtimpath'.
---
---@return table Array with specifications of all plugins registered in
--- current session.
MiniDeps.get_session = function()
-- Normalize `H.session` allowing specs for same plugin
local res, plugin_ids = {}, {}
local add_spec = function(spec)
local id = plugin_ids[spec.path] or (#res + 1)
-- Treat `depends` differently as it is an array and direct merge is bad
-- Also: https://github.com/neovim/neovim/pull/15094#discussion_r671663938
local depends = vim.deepcopy((res[id] or {}).depends or {})
vim.list_extend(depends, spec.depends or {})
res[id] = vim.tbl_deep_extend('force', res[id] or {}, spec)
res[id].depends = depends
plugin_ids[spec.path] = id
end
vim.tbl_map(add_spec, H.session)
H.session = res
-- Add 'start/' plugins that are in 'rtp'. NOTE: not whole session concept is
-- built around presence in 'rtp' to 100% ensure to preserve the order in
-- which user called `add()`.
local start_path = H.full_path(H.get_package_path() .. '/pack/deps/start')
local pattern = string.format('^%s/([^/]+)$', vim.pesc(start_path))
for _, runtime_path in ipairs(vim.api.nvim_list_runtime_paths()) do
-- Make sure plugin path is normalized (matters on Windows)
local path = H.full_path(runtime_path)
local name = string.match(path, pattern)
if name ~= nil then add_spec({ path = path, name = name, hooks = {}, depends = {} }) end
end
-- Return copy to not allow modification in place
return vim.deepcopy(res)
end
--- Execute function now
---
--- Safely execute function immediately. Errors are shown with |vim.notify()|
--- later, after all queued functions (including with |MiniDeps.later()|)
--- are executed, thus not blocking execution of next code in file.
---
--- Assumed to be used as a first step during two-stage config execution to
--- load plugins immediately during startup. See |MiniDeps-overview|.
---
---@param f function Callable to execute.
MiniDeps.now = function(f)
local ok, err = pcall(f)
if not ok then table.insert(H.cache.exec_errors, err) end
H.schedule_finish()
end
--- Execute function later
---
--- Queue function to be safely executed later without blocking execution of
--- next code in file. All queued functions are guaranteed to be executed in
--- order they were added.
--- Errors are shown with |vim.notify()| after all queued functions are executed.
---
--- Assumed to be used as a second step during two-stage config execution to
--- load plugins "lazily" after startup. See |MiniDeps-overview|.
---
---@param f function Callable to execute.
MiniDeps.later = function(f)
table.insert(H.cache.later_callback_queue, f)
H.schedule_finish()
end
-- Helper data ================================================================
-- Module default config
H.default_config = MiniDeps.config
-- Array of plugin specs
H.session = {}
-- Various cache
H.cache = {
-- Whether finish of `now()` or `later()` is already scheduled
finish_is_scheduled = false,
-- Callback queue for `later()`
later_callback_queue = {},
-- Errors during execution of `now()` or `later()`
exec_errors = {},
-- Git version
git_version = nil,
}
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
H.check_type('config', config, 'table', true)
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
H.check_type('job', config.job, 'table')
H.check_type('job.n_threads', config.job.n_threads, 'number', true)
H.check_type('job.timeout', config.job.timeout, 'number')
H.check_type('path', config.path, 'table')
H.check_type('path.package', config.path.package, 'string')
H.check_type('path.snapshot', config.path.snapshot, 'string')
H.check_type('path.log', config.path.log, 'string')
H.check_type('silent', config.silent, 'boolean')
return config
end
H.apply_config = function(config)
MiniDeps.config = config
-- Reset current session to allow resourcing script with `setup()` call
H.session = {}
-- Add target package path to 'packpath'
local pack_path = H.full_path(config.path.package)
vim.cmd('set packpath^=' .. vim.fn.fnameescape(pack_path))
end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniDeps.config, vim.b.minideps_config or {}, config or {})
end
H.create_autocommands = function()
local gr = vim.api.nvim_create_augroup('MiniDeps', {})
vim.api.nvim_create_autocmd('ColorScheme', { group = gr, callback = H.create_default_hl, desc = 'Ensure colors' })
end
--stylua: ignore
H.create_default_hl = function()
local hi = function(name, opts)
opts.default = true
vim.api.nvim_set_hl(0, name, opts)
end
local has_core_diff_hl = vim.fn.has('nvim-0.10') == 1
hi('MiniDepsChangeAdded', { link = has_core_diff_hl and 'Added' or 'diffAdded' })
hi('MiniDepsChangeRemoved', { link = has_core_diff_hl and 'Removed' or 'diffRemoved' })
hi('MiniDepsHint', { link = 'DiagnosticHint' })
hi('MiniDepsInfo', { link = 'DiagnosticInfo' })
hi('MiniDepsMsgBreaking', { link = 'DiagnosticWarn' })
hi('MiniDepsPlaceholder', { link = 'Comment' })
hi('MiniDepsTitle', { link = 'Title' })
hi('MiniDepsTitleError', { link = 'DiffDelete' })
hi('MiniDepsTitleSame', { link = 'DiffText' })
hi('MiniDepsTitleUpdate', { link = 'DiffAdd' })
end
H.create_user_commands = function()
-- Do not create commands immediately to increase startup time
local new_cmd = vim.schedule_wrap(vim.api.nvim_create_user_command)
local complete_session_names = function(arg, _, _)
local session_names = vim.tbl_map(function(s) return s.name end, MiniDeps.get_session())
return vim.tbl_filter(function(n) return vim.startswith(n, arg) end, session_names)
end
local complete_disk_names = function(arg, _, _)
local disk_names = vim.tbl_map(function(p) return vim.fn.fnamemodify(p, ':t') end, H.get_all_plugin_paths())
return vim.tbl_filter(function(n) return vim.startswith(n, arg) end, disk_names)
end
local add = function(input) MiniDeps.add(input.fargs[1]) end
new_cmd('DepsAdd', add, { nargs = '+', complete = complete_disk_names, desc = 'Add plugin to session' })
local make_update_cmd = function(name, offline, desc)
local callback = function(input)
local names
if #input.fargs > 0 then names = input.fargs end
MiniDeps.update(names, { force = input.bang, offline = offline })
end
local opts = { bang = true, complete = complete_session_names, nargs = '*', desc = desc }
new_cmd(name, callback, opts)
end
make_update_cmd('DepsUpdate', false, 'Update plugins')
make_update_cmd('DepsUpdateOffline', true, 'Update plugins without downloading from source')
local show_log = function()
H.edit(H.get_config().path.log)
H.update_add_syntax()
vim.cmd([[syntax match MiniDepsTitle "^\(==========\).*\1$"]])
end
new_cmd('DepsShowLog', show_log, { desc = 'Show log' })
local clean = function(input) MiniDeps.clean({ force = input.bang }) end
new_cmd('DepsClean', clean, { bang = true, desc = 'Delete unused plugins' })
local snap_save = function(input) MiniDeps.snap_save(input.fargs[1]) end
new_cmd('DepsSnapSave', snap_save, { nargs = '*', complete = 'file', desc = 'Save plugin snapshot' })
local snap_load = function(input) MiniDeps.snap_load(input.fargs[1]) end
new_cmd('DepsSnapLoad', snap_load, { nargs = '*', complete = 'file', desc = 'Load plugin snapshot' })
end
-- Git commands ---------------------------------------------------------------
H.git_cmd = function(cmd_name, ...)
local args = H.git_args[cmd_name](...)
if args == nil then return {} end
-- Use '-c gc.auto=0' to disable `stderr` "Auto packing..." messages
return { 'git', '-c', 'gc.auto=0', unpack(args) }
end
--stylua: ignore
H.git_args = {
version = function()
return { 'version' }
end,
clone = function(source, path)
local res = {
'clone', '--quiet', '--filter=blob:none',
'--recurse-submodules', '--also-filter-submodules', '--origin', 'origin',
source, path,
}
-- Use `--also-filter-submodules` only with appropriate version
if not (H.cache.git_version.major >= 2 and H.cache.git_version.minor >= 36) then
table.remove(res, 5)
end
return res
end,
stash = function(timestamp)
return { 'stash', '--quiet', '--message', '(mini.deps) ' .. timestamp .. ' Stash before checkout' }
end,
checkout = function(target)
return { 'checkout', '--quiet', target }
end,
-- Using '--tags --force' means conflicting tags will be synced with remote
fetch = function()
return { 'fetch', '--quiet', '--tags', '--force', '--recurse-submodules=yes', 'origin' }
end,
set_origin = function(source)
return { 'remote', 'set-url', 'origin', source }
end,
get_origin = function()
return { 'remote', 'get-url', 'origin' }
end,
get_default_origin_branch = function()
return { 'rev-parse', '--abbrev-ref', 'origin/HEAD' }
end,
is_origin_branch = function(name)
-- Returns branch's name if it is present
return { 'branch', '--list', '--all', '--format=%(refname:short)', 'origin/' .. name }
end,
-- Using `rev-list -1` shows a commit of revision, while `rev-parse` shows
-- hash of revision. Those are different for annotated tags.
get_hash = function(rev)
return { 'rev-list', '-1', rev }
end,
log = function(from, to)
if from == nil or to == nil or from == to then return nil end
-- `--topo-order` makes showing divergent branches nicer
-- `--decorate-refs` shows only tags near commits (not `origin/main`, etc.)
--stylua: ignore
return {
'log', '--pretty=format:%m %h | %ai | %an%d%n %s%n', '--topo-order', '--decorate-refs=refs/tags',
from .. '...' .. to,
}
end,
}
H.ensure_git_exec = function()
if H.cache.git_version ~= nil then return end
local jobs = { H.cli_new_job(H.git_cmd('version'), vim.fn.getcwd()) }
H.cli_run(jobs)
if #jobs[1].err > 0 then H.error('Could not find executable `git` CLI tool') end
local major, minor = string.match(H.cli_stream_tostring(jobs[1].out), '(%d+)%.(%d+)')
H.cache.git_version = { major = tonumber(major), minor = tonumber(minor) }
end
-- Plugin specification -------------------------------------------------------
H.expand_spec = function(target, spec)
-- Prepare
if type(spec) == 'string' then
local field = string.find(spec, '/') ~= nil and 'source' or 'name'
spec = { [field] = spec }
end
if type(spec) ~= 'table' then H.error('Plugin spec should be table.') end
local has_min_fields = type(spec.source) == 'string' or type(spec.name) == 'string'
if not has_min_fields then H.error('Plugin spec should have proper `source` or `name`.') end
-- Normalize
spec = vim.deepcopy(spec)
if spec.source and type(spec.source) ~= 'string' then H.error('`source` in plugin spec should be string.') end
local is_user_repo = type(spec.source) == 'string' and spec.source:find('^[%w-]+/[%w-_.]+$') ~= nil
if is_user_repo then spec.source = 'https://github.com/' .. spec.source end
spec.name = spec.name or vim.fn.fnamemodify(spec.source, ':t')
if type(spec.name) ~= 'string' then H.error('`name` in plugin spec should be string.') end
if string.find(spec.name, '/') ~= nil then H.error('`name` in plugin spec should not contain "/".') end
if spec.name == '' then H.error('`name` in plugin spec should not be empty.') end
if spec.checkout and type(spec.checkout) ~= 'string' then H.error('`checkout` in plugin spec should be string.') end
if spec.monitor and type(spec.monitor) ~= 'string' then H.error('`monitor` in plugin spec should be string.') end
spec.hooks = vim.deepcopy(spec.hooks) or {}
if type(spec.hooks) ~= 'table' then H.error('`hooks` in plugin spec should be table.') end
local hook_names = { 'pre_install', 'post_install', 'pre_checkout', 'post_checkout' }
for _, hook_name in ipairs(hook_names) do
local is_not_hook = spec.hooks[hook_name] and not vim.is_callable(spec.hooks[hook_name])
if is_not_hook then H.error('`hooks.' .. hook_name .. '` in plugin spec should be callable.') end
end
-- Expand dependencies recursively before adding current spec to target
spec.depends = vim.deepcopy(spec.depends) or {}
if not H.islist(spec.depends) then H.error('`depends` in plugin spec should be array.') end
for _, dep_spec in ipairs(spec.depends) do
H.expand_spec(target, dep_spec)