on master help sign in
Show (git) diff signs in gutter (#3890)
5a3ff74Pascal Kuthe
Cargo.lock+868−10
@@ -2,6 +2,12 @@
2# It is not intended for manual editing.
2# It is not intended for manual editing.
3version = 3
3version = 3
4
4
5+[[package]]
6+name = "adler"
7+version = "1.0.2"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
10+
5[[package]]
11[[package]]
6name = "ahash"
12name = "ahash"
7version = "0.7.6"
13version = "0.7.6"
@@ -55,6 +61,15 @@ version = "1.5.1"
55source = "registry+https://github.com/rust-lang/crates.io-index"
61source = "registry+https://github.com/rust-lang/crates.io-index"
56checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
62checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
57
63
64+[[package]]
65+name = "atoi"
66+version = "1.0.0"
67+source = "registry+https://github.com/rust-lang/crates.io-index"
68+checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e"
69+dependencies = [
70+ "num-traits",
71+]
72+
58[[package]]
73[[package]]
59name = "autocfg"
74name = "autocfg"
60version = "1.1.0"
75version = "1.1.0"
@@ -78,12 +93,43 @@ dependencies = [
78 "regex-automata",
93 "regex-automata",
79]
94]
80
95
96+[[package]]
97+name = "bstr"
98+version = "1.0.1"
99+source = "registry+https://github.com/rust-lang/crates.io-index"
100+checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd"
101+dependencies = [
102+ "memchr",
103+ "once_cell",
104+ "regex-automata",
105+ "serde",
106+]
107+
108+[[package]]
109+name = "btoi"
110+version = "0.4.2"
111+source = "registry+https://github.com/rust-lang/crates.io-index"
112+checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11"
113+dependencies = [
114+ "num-traits",
115+]
116+
81[[package]]
117[[package]]
82name = "bumpalo"
118name = "bumpalo"
83version = "3.11.1"
119version = "3.11.1"
84source = "registry+https://github.com/rust-lang/crates.io-index"
120source = "registry+https://github.com/rust-lang/crates.io-index"
85checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
121checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
86
122
123+[[package]]
124+name = "byte-unit"
125+version = "4.0.17"
126+source = "registry+https://github.com/rust-lang/crates.io-index"
127+checksum = "581ad4b3d627b0c09a0ccb2912148f839acaca0b93cf54cbe42b6c674e86079c"
128+dependencies = [
129+ "serde",
130+ "utf8-width",
131+]
132+
87[[package]]
133[[package]]
88name = "bytecount"
134name = "bytecount"
89version = "0.6.3"
135version = "0.6.3"
@@ -96,12 +142,27 @@ version = "1.3.0"
96source = "registry+https://github.com/rust-lang/crates.io-index"
142source = "registry+https://github.com/rust-lang/crates.io-index"
97checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
143checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
98
144
145+[[package]]
146+name = "bytesize"
147+version = "1.1.0"
148+source = "registry+https://github.com/rust-lang/crates.io-index"
149+checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70"
150+
99[[package]]
151[[package]]
100name = "cassowary"
152name = "cassowary"
101version = "0.3.0"
153version = "0.3.0"
102source = "registry+https://github.com/rust-lang/crates.io-index"
154source = "registry+https://github.com/rust-lang/crates.io-index"
103checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
155checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
104
156
157+[[package]]
158+name = "castaway"
159+version = "0.2.2"
160+source = "registry+https://github.com/rust-lang/crates.io-index"
161+checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
162+dependencies = [
163+ "rustversion",
164+]
165+
105[[package]]
166[[package]]
106name = "cc"
167name = "cc"
107version = "1.0.77"
168version = "1.0.77"
@@ -148,6 +209,12 @@ dependencies = [
148 "winapi",
209 "winapi",
149]
210]
150
211
212+[[package]]
213+name = "clru"
214+version = "0.5.0"
215+source = "registry+https://github.com/rust-lang/crates.io-index"
216+checksum = "218d6bd3dde8e442a975fa1cd233c0e5fded7596bccfe39f58eca98d22421e0a"
217+
151[[package]]
218[[package]]
152name = "codespan-reporting"
219name = "codespan-reporting"
153version = "0.11.1"
220version = "0.11.1"
@@ -158,6 +225,17 @@ dependencies = [
158 "unicode-width",
225 "unicode-width",
159]
226]
160
227
228+[[package]]
229+name = "compact_str"
230+version = "0.6.1"
231+source = "registry+https://github.com/rust-lang/crates.io-index"
232+checksum = "5138945395949e7dfba09646dc9e766b548ff48e23deb5246890e6b64ae9e1b9"
233+dependencies = [
234+ "castaway",
235+ "itoa",
236+ "ryu",
237+]
238+
161[[package]]
239[[package]]
162name = "content_inspector"
240name = "content_inspector"
163version = "0.2.4"
241version = "0.2.4"
@@ -173,6 +251,15 @@ version = "0.8.3"
173source = "registry+https://github.com/rust-lang/crates.io-index"
251source = "registry+https://github.com/rust-lang/crates.io-index"
174checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
252checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
175
253
254+[[package]]
255+name = "crc32fast"
256+version = "1.3.2"
257+source = "registry+https://github.com/rust-lang/crates.io-index"
258+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
259+dependencies = [
260+ "cfg-if",
261+]
262+
176[[package]]
263[[package]]
177name = "crossbeam-utils"
264name = "crossbeam-utils"
178version = "0.8.14"
265version = "0.8.14"
@@ -252,6 +339,28 @@ dependencies = [
252 "syn",
339 "syn",
253]
340]
254
341
342+[[package]]
343+name = "dashmap"
344+version = "5.4.0"
345+source = "registry+https://github.com/rust-lang/crates.io-index"
346+checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
347+dependencies = [
348+ "cfg-if",
349+ "hashbrown 0.12.3",
350+ "lock_api",
351+ "once_cell",
352+ "parking_lot_core",
353+]
354+
355+[[package]]
356+name = "dirs"
357+version = "4.0.0"
358+source = "registry+https://github.com/rust-lang/crates.io-index"
359+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
360+dependencies = [
361+ "dirs-sys",
362+]
363+
255[[package]]
364[[package]]
256name = "dirs-next"
365name = "dirs-next"
257version = "2.0.0"
366version = "2.0.0"
@@ -262,6 +371,17 @@ dependencies = [
262 "dirs-sys-next",
371 "dirs-sys-next",
263]
372]
264
373
374+[[package]]
375+name = "dirs-sys"
376+version = "0.3.7"
377+source = "registry+https://github.com/rust-lang/crates.io-index"
378+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
379+dependencies = [
380+ "libc",
381+ "redox_users",
382+ "winapi",
383+]
384+
265[[package]]
385[[package]]
266name = "dirs-sys-next"
386name = "dirs-sys-next"
267version = "0.1.2"
387version = "0.1.2"
@@ -336,6 +456,28 @@ dependencies = [
336 "log",
456 "log",
337]
457]
338
458
459+[[package]]
460+name = "filetime"
461+version = "0.2.18"
462+source = "registry+https://github.com/rust-lang/crates.io-index"
463+checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
464+dependencies = [
465+ "cfg-if",
466+ "libc",
467+ "redox_syscall",
468+ "windows-sys",
469+]
470+
471+[[package]]
472+name = "flate2"
473+version = "1.0.25"
474+source = "registry+https://github.com/rust-lang/crates.io-index"
475+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
476+dependencies = [
477+ "crc32fast",
478+ "miniz_oxide",
479+]
480+
339[[package]]
481[[package]]
340name = "fnv"
482name = "fnv"
341version = "1.0.7"
483version = "1.0.7"
@@ -407,6 +549,498 @@ dependencies = [
407 "wasi",
549 "wasi",
408]
550]
409
551
552+[[package]]
553+name = "git-actor"
554+version = "0.13.0"
555+source = "registry+https://github.com/rust-lang/crates.io-index"
556+checksum = "18d4ce09c0a6c71c044700e5932877667f427f007b77e6c39ab49aebc4719e25"
557+dependencies = [
558+ "bstr 1.0.1",
559+ "btoi",
560+ "git-date",
561+ "itoa",
562+ "nom",
563+ "quick-error",
564+]
565+
566+[[package]]
567+name = "git-attributes"
568+version = "0.5.0"
569+source = "registry+https://github.com/rust-lang/crates.io-index"
570+checksum = "6c62e66a042c6b39c6dbfa3be37d134900d99ff9c54bbe489ed560a573895d5d"
571+dependencies = [
572+ "bstr 1.0.1",
573+ "compact_str",
574+ "git-features",
575+ "git-glob",
576+ "git-path",
577+ "git-quote",
578+ "thiserror",
579+ "unicode-bom",
580+]
581+
582+[[package]]
583+name = "git-bitmap"
584+version = "0.1.2"
585+source = "registry+https://github.com/rust-lang/crates.io-index"
586+checksum = "327098a7ad27ae298d7e71602dbd4375cc828d755d10a720e4be0be1b4ec38f0"
587+dependencies = [
588+ "quick-error",
589+]
590+
591+[[package]]
592+name = "git-chunk"
593+version = "0.3.2"
594+source = "registry+https://github.com/rust-lang/crates.io-index"
595+checksum = "07b2bc1635b660ad6e30379a84a4946590a3c124b747107c2cca1d9dbb98f588"
596+dependencies = [
597+ "thiserror",
598+]
599+
600+[[package]]
601+name = "git-command"
602+version = "0.1.0"
603+source = "registry+https://github.com/rust-lang/crates.io-index"
604+checksum = "9e4b01997b6551554fdac6f02277d0d04c3e869daa649bedd06d38c86f11dc42"
605+dependencies = [
606+ "bstr 1.0.1",
607+]
608+
609+[[package]]
610+name = "git-config"
611+version = "0.10.0"
612+source = "registry+https://github.com/rust-lang/crates.io-index"
613+checksum = "bd8603e953bd4c9bf310e74e43697400f5542f1cc75fad46fbd7427135a9534f"
614+dependencies = [
615+ "bstr 1.0.1",
616+ "git-config-value",
617+ "git-features",
618+ "git-glob",
619+ "git-path",
620+ "git-ref",
621+ "git-sec",
622+ "memchr",
623+ "nom",
624+ "once_cell",
625+ "smallvec",
626+ "thiserror",
627+ "unicode-bom",
628+]
629+
630+[[package]]
631+name = "git-config-value"
632+version = "0.8.2"
633+source = "registry+https://github.com/rust-lang/crates.io-index"
634+checksum = "05f276bfe5806b414915112f1eec0f006206cdf5b8cc9bbb44ef7e52286dc3eb"
635+dependencies = [
636+ "bitflags",
637+ "bstr 1.0.1",
638+ "git-path",
639+ "libc",
640+ "thiserror",
641+]
642+
643+[[package]]
644+name = "git-credentials"
645+version = "0.6.1"
646+source = "registry+https://github.com/rust-lang/crates.io-index"
647+checksum = "3f540186ea56fd075ba2b923180ebf4318e66ceaeac0a2a518e75dab8517d339"
648+dependencies = [
649+ "bstr 1.0.1",
650+ "git-command",
651+ "git-config-value",
652+ "git-path",
653+ "git-prompt",
654+ "git-sec",
655+ "git-url",
656+ "thiserror",
657+]
658+
659+[[package]]
660+name = "git-date"
661+version = "0.2.0"
662+source = "registry+https://github.com/rust-lang/crates.io-index"
663+checksum = "37881e9725df41e15d16216d3a0cee251fd8a39d425f75b389112df5c7f20f3d"
664+dependencies = [
665+ "bstr 1.0.1",
666+ "itoa",
667+ "thiserror",
668+ "time",
669+]
670+
671+[[package]]
672+name = "git-diff"
673+version = "0.21.0"
674+source = "registry+https://github.com/rust-lang/crates.io-index"
675+checksum = "0a88666a0ae4365b55a0cbf2efde68d2a4cff0747894ad229403bd60b0b2abc5"
676+dependencies = [
677+ "git-hash",
678+ "git-object",
679+ "imara-diff",
680+ "thiserror",
681+]
682+
683+[[package]]
684+name = "git-discover"
685+version = "0.7.0"
686+source = "registry+https://github.com/rust-lang/crates.io-index"
687+checksum = "881e4136d5599cfdb79d8ef60d650823d1a563589fa493d8e4961e64d78a79f2"
688+dependencies = [
689+ "bstr 1.0.1",
690+ "git-hash",
691+ "git-path",
692+ "git-ref",
693+ "git-sec",
694+ "thiserror",
695+]
696+
697+[[package]]
698+name = "git-features"
699+version = "0.23.1"
700+source = "registry+https://github.com/rust-lang/crates.io-index"
701+checksum = "4be88ae837674c71b30c6517c6f5f1335f8135bb8a9ffef20000d211933bed08"
702+dependencies = [
703+ "crc32fast",
704+ "flate2",
705+ "git-hash",
706+ "libc",
707+ "once_cell",
708+ "prodash",
709+ "quick-error",
710+ "sha1_smol",
711+ "walkdir",
712+]
713+
714+[[package]]
715+name = "git-glob"
716+version = "0.4.2"
717+source = "registry+https://github.com/rust-lang/crates.io-index"
718+checksum = "6d756430237112f8c89049236f60fdcdb0005127b1f7e531d40984e4fe7daa90"
719+dependencies = [
720+ "bitflags",
721+ "bstr 1.0.1",
722+]
723+
724+[[package]]
725+name = "git-hash"
726+version = "0.9.11"
727+source = "registry+https://github.com/rust-lang/crates.io-index"
728+checksum = "16d46e6c2d1e8da4438a87bf516a6761b300964a353541fea61e96b3c7b34554"
729+dependencies = [
730+ "hex",
731+ "thiserror",
732+]
733+
734+[[package]]
735+name = "git-index"
736+version = "0.7.1"
737+source = "registry+https://github.com/rust-lang/crates.io-index"
738+checksum = "821583c2d12b1e864694eb0bf1cca10ff6a3f45966f5f834e0f921b496dbe7cb"
739+dependencies = [
740+ "atoi",
741+ "bitflags",
742+ "bstr 1.0.1",
743+ "filetime",
744+ "git-bitmap",
745+ "git-features",
746+ "git-hash",
747+ "git-lock",
748+ "git-object",
749+ "git-traverse",
750+ "itoa",
751+ "memmap2",
752+ "smallvec",
753+ "thiserror",
754+]
755+
756+[[package]]
757+name = "git-lock"
758+version = "2.2.0"
759+source = "registry+https://github.com/rust-lang/crates.io-index"
760+checksum = "5f0fe10bf961f62b1335b4c07785e64fb4d86c5ed367dc7cd9360f13c3eb7c78"
761+dependencies = [
762+ "fastrand",
763+ "git-tempfile",
764+ "quick-error",
765+]
766+
767+[[package]]
768+name = "git-mailmap"
769+version = "0.5.0"
770+source = "registry+https://github.com/rust-lang/crates.io-index"
771+checksum = "bb3f85ce84b2328aeb3124a809f7b3a63e59c4d63c227dba7a9cdf6fca6c0987"
772+dependencies = [
773+ "bstr 1.0.1",
774+ "git-actor",
775+ "quick-error",
776+]
777+
778+[[package]]
779+name = "git-object"
780+version = "0.22.1"
781+source = "registry+https://github.com/rust-lang/crates.io-index"
782+checksum = "9469a8c00d8bb500ee76a12e455bb174b4ddf71674713335dd1a84313723f7b3"
783+dependencies = [
784+ "bstr 1.0.1",
785+ "btoi",
786+ "git-actor",
787+ "git-features",
788+ "git-hash",
789+ "git-validate",
790+ "hex",
791+ "itoa",
792+ "nom",
793+ "smallvec",
794+ "thiserror",
795+]
796+
797+[[package]]
798+name = "git-odb"
799+version = "0.35.0"
800+source = "registry+https://github.com/rust-lang/crates.io-index"
801+checksum = "aaaea7031ac7d8dfee232a16d7114395d118226214fb03fe4e15d1f4d62a88a6"
802+dependencies = [
803+ "arc-swap",
804+ "git-features",
805+ "git-hash",
806+ "git-object",
807+ "git-pack",
808+ "git-path",
809+ "git-quote",
810+ "parking_lot",
811+ "tempfile",
812+ "thiserror",
813+]
814+
815+[[package]]
816+name = "git-pack"
817+version = "0.25.0"
818+source = "registry+https://github.com/rust-lang/crates.io-index"
819+checksum = "bc4386dff835ffdc3697c3558111f708fd7b7695c42a4347f2d211cf3246c8e1"
820+dependencies = [
821+ "bytesize",
822+ "clru",
823+ "dashmap",
824+ "git-chunk",
825+ "git-diff",
826+ "git-features",
827+ "git-hash",
828+ "git-object",
829+ "git-path",
830+ "git-tempfile",
831+ "git-traverse",
832+ "hash_hasher",
833+ "memmap2",
834+ "parking_lot",
835+ "smallvec",
836+ "thiserror",
837+]
838+
839+[[package]]
840+name = "git-path"
841+version = "0.5.0"
842+source = "registry+https://github.com/rust-lang/crates.io-index"
843+checksum = "425dc1022690be13e6c5bde4b7e04d9504d323605ec314cd367cebf38a812572"
844+dependencies = [
845+ "bstr 1.0.1",
846+ "thiserror",
847+]
848+
849+[[package]]
850+name = "git-prompt"
851+version = "0.1.1"
852+source = "registry+https://github.com/rust-lang/crates.io-index"
853+checksum = "fa6947935c0671342277bc883ff0687978477b570c1ffe2200b9ba5ac8afdd9f"
854+dependencies = [
855+ "git-command",
856+ "git-config-value",
857+ "nix",
858+ "parking_lot",
859+ "thiserror",
860+]
861+
862+[[package]]
863+name = "git-quote"
864+version = "0.3.0"
865+source = "registry+https://github.com/rust-lang/crates.io-index"
866+checksum = "5ea17931d07cbe447f371bbdf45ff03c30ea86db43788166655a5302df87ecfc"
867+dependencies = [
868+ "bstr 1.0.1",
869+ "btoi",
870+ "quick-error",
871+]
872+
873+[[package]]
874+name = "git-ref"
875+version = "0.18.0"
876+source = "registry+https://github.com/rust-lang/crates.io-index"
877+checksum = "638c9e454bacb2965a43f05b4a383c8f66dc64f3a770bd0324b221c2a20e121d"
878+dependencies = [
879+ "git-actor",
880+ "git-features",
881+ "git-hash",
882+ "git-lock",
883+ "git-object",
884+ "git-path",
885+ "git-tempfile",
886+ "git-validate",
887+ "memmap2",
888+ "nom",
889+ "thiserror",
890+]
891+
892+[[package]]
893+name = "git-refspec"
894+version = "0.3.1"
895+source = "registry+https://github.com/rust-lang/crates.io-index"
896+checksum = "9497af773538ae8cfda053ff7dd0a9e6c28d333ba653040f54b8b4ee32f14187"
897+dependencies = [
898+ "bstr 1.0.1",
899+ "git-hash",
900+ "git-revision",
901+ "git-validate",
902+ "smallvec",
903+ "thiserror",
904+]
905+
906+[[package]]
907+name = "git-repository"
908+version = "0.26.0"
909+source = "registry+https://github.com/rust-lang/crates.io-index"
910+checksum = "eeb43e59612e493af6a433bf0a960de0042c8aa6f4e4c4cb414f03b97e296b82"
911+dependencies = [
912+ "byte-unit",
913+ "clru",
914+ "git-actor",
915+ "git-attributes",
916+ "git-config",
917+ "git-credentials",
918+ "git-date",
919+ "git-diff",
920+ "git-discover",
921+ "git-features",
922+ "git-glob",
923+ "git-hash",
924+ "git-index",
925+ "git-lock",
926+ "git-mailmap",
927+ "git-object",
928+ "git-odb",
929+ "git-pack",
930+ "git-path",
931+ "git-prompt",
932+ "git-ref",
933+ "git-refspec",
934+ "git-revision",
935+ "git-sec",
936+ "git-tempfile",
937+ "git-traverse",
938+ "git-url",
939+ "git-validate",
940+ "git-worktree",
941+ "log",
942+ "once_cell",
943+ "signal-hook",
944+ "smallvec",
945+ "thiserror",
946+ "unicode-normalization",
947+]
948+
949+[[package]]
950+name = "git-revision"
951+version = "0.6.0"
952+source = "registry+https://github.com/rust-lang/crates.io-index"
953+checksum = "1efd31c63c3745b5dba5ec7109eec41a9c717f4e1e797fe0ef93098f33f31b25"
954+dependencies = [
955+ "bstr 1.0.1",
956+ "git-date",
957+ "git-hash",
958+ "git-object",
959+ "hash_hasher",
960+ "thiserror",
961+]
962+
963+[[package]]
964+name = "git-sec"
965+version = "0.4.2"
966+source = "registry+https://github.com/rust-lang/crates.io-index"
967+checksum = "8c79769f6546814d0774db7295c768441016b7e40bdd414fa8dfae2c616a1892"
968+dependencies = [
969+ "bitflags",
970+ "dirs",
971+ "git-path",
972+ "libc",
973+ "windows",
974+]
975+
976+[[package]]
977+name = "git-tempfile"
978+version = "2.0.6"
979+source = "registry+https://github.com/rust-lang/crates.io-index"
980+checksum = "2d23bc6129de3cbd81e6c9d0d685b5540c6b41bd9fa0cc38f381bc300743d708"
981+dependencies = [
982+ "dashmap",
983+ "libc",
984+ "once_cell",
985+ "signal-hook",
986+ "signal-hook-registry",
987+ "tempfile",
988+]
989+
990+[[package]]
991+name = "git-traverse"
992+version = "0.18.0"
993+source = "registry+https://github.com/rust-lang/crates.io-index"
994+checksum = "0d0c4dd773c69f294f43ace8373d48eb770129791f104c6857fa8cac0505af89"
995+dependencies = [
996+ "git-hash",
997+ "git-object",
998+ "hash_hasher",
999+ "thiserror",
1000+]
1001+
1002+[[package]]
1003+name = "git-url"
1004+version = "0.10.1"
1005+source = "registry+https://github.com/rust-lang/crates.io-index"
1006+checksum = "21b7f8323196840e7932f5b60e1d9c1d6c140fd806bc512f8beedc3f990a1f81"
1007+dependencies = [
1008+ "bstr 1.0.1",
1009+ "git-features",
1010+ "git-path",
1011+ "home",
1012+ "thiserror",
1013+ "url",
1014+]
1015+
1016+[[package]]
1017+name = "git-validate"
1018+version = "0.6.0"
1019+source = "registry+https://github.com/rust-lang/crates.io-index"
1020+checksum = "b5439d6aa0de838dfadd74a71e97a9e23ebc719fd11a9ab6788b835b112c8c3d"
1021+dependencies = [
1022+ "bstr 1.0.1",
1023+ "thiserror",
1024+]
1025+
1026+[[package]]
1027+name = "git-worktree"
1028+version = "0.7.0"
1029+source = "registry+https://github.com/rust-lang/crates.io-index"
1030+checksum = "45bcc69c36a29cfa283710b7901877ab251d658935f5a41ed824416af500e0ed"
1031+dependencies = [
1032+ "bstr 1.0.1",
1033+ "git-attributes",
1034+ "git-features",
1035+ "git-glob",
1036+ "git-hash",
1037+ "git-index",
1038+ "git-object",
1039+ "git-path",
1040+ "io-close",
1041+ "thiserror",
1042+]
1043+
410[[package]]
1044[[package]]
411name = "globset"
1045name = "globset"
412version = "0.4.9"
1046version = "0.4.9"
@@ -414,7 +1048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
414checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
1048checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
415dependencies = [
1049dependencies = [
416 "aho-corasick",
1050 "aho-corasick",
417− "bstr",
1051+ "bstr 0.2.17",
418 "fnv",
1052 "fnv",
419 "log",
1053 "log",
420 "regex",
1054 "regex",
@@ -436,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
436checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e"
1070checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e"
437dependencies = [
1071dependencies = [
438 "aho-corasick",
1072 "aho-corasick",
439− "bstr",
1073+ "bstr 0.2.17",
440 "grep-matcher",
1074 "grep-matcher",
441 "log",
1075 "log",
442 "regex",
1076 "regex",
@@ -450,7 +1084,7 @@ version = "0.1.10"
450source = "registry+https://github.com/rust-lang/crates.io-index"
1084source = "registry+https://github.com/rust-lang/crates.io-index"
451checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc"
1085checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc"
452dependencies = [
1086dependencies = [
453− "bstr",
1087+ "bstr 0.2.17",
454 "bytecount",
1088 "bytecount",
455 "encoding_rs",
1089 "encoding_rs",
456 "encoding_rs_io",
1090 "encoding_rs_io",
@@ -459,6 +1093,12 @@ dependencies = [
459 "memmap2",
1093 "memmap2",
460]
1094]
461
1095
1096+[[package]]
1097+name = "hash_hasher"
1098+version = "2.0.3"
1099+source = "registry+https://github.com/rust-lang/crates.io-index"
1100+checksum = "74721d007512d0cb3338cd20f0654ac913920061a4c4d0d8708edb3f2a698c0c"
1101+
462[[package]]
1102[[package]]
463name = "hashbrown"
1103name = "hashbrown"
464version = "0.12.3"
1104version = "0.12.3"
@@ -576,6 +1216,7 @@ dependencies = [
576 "helix-loader",
1216 "helix-loader",
577 "helix-lsp",
1217 "helix-lsp",
578 "helix-tui",
1218 "helix-tui",
1219+ "helix-vcs",
579 "helix-view",
1220 "helix-view",
580 "ignore",
1221 "ignore",
581 "indoc",
1222 "indoc",
@@ -608,6 +1249,19 @@ dependencies = [
608 "unicode-segmentation",
1249 "unicode-segmentation",
609]
1250]
610
1251
1252+[[package]]
1253+name = "helix-vcs"
1254+version = "0.6.0"
1255+dependencies = [
1256+ "git-repository",
1257+ "helix-core",
1258+ "imara-diff",
1259+ "log",
1260+ "parking_lot",
1261+ "tempfile",
1262+ "tokio",
1263+]
1264+
611[[package]]
1265[[package]]
612name = "helix-view"
1266name = "helix-view"
613version = "0.6.0"
1267version = "0.6.0"
@@ -624,6 +1278,7 @@ dependencies = [
624 "helix-loader",
1278 "helix-loader",
625 "helix-lsp",
1279 "helix-lsp",
626 "helix-tui",
1280 "helix-tui",
1281+ "helix-vcs",
627 "log",
1282 "log",
628 "once_cell",
1283 "once_cell",
629 "serde",
1284 "serde",
@@ -645,6 +1300,27 @@ dependencies = [
645 "libc",
1300 "libc",
646]
1301]
647
1302
1303+[[package]]
1304+name = "hex"
1305+version = "0.4.3"
1306+source = "registry+https://github.com/rust-lang/crates.io-index"
1307+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
1308+
1309+[[package]]
1310+name = "home"
1311+version = "0.5.4"
1312+source = "registry+https://github.com/rust-lang/crates.io-index"
1313+checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408"
1314+dependencies = [
1315+ "winapi",
1316+]
1317+
1318+[[package]]
1319+name = "human_format"
1320+version = "1.0.3"
1321+source = "registry+https://github.com/rust-lang/crates.io-index"
1322+checksum = "86cce260d758a9aa3d7c4b99d55c815a540f8a37514ba6046ab6be402a157cb0"
1323+
648[[package]]
1324[[package]]
649name = "iana-time-zone"
1325name = "iana-time-zone"
650version = "0.1.53"
1326version = "0.1.53"
@@ -722,6 +1398,16 @@ dependencies = [
722 "cfg-if",
1398 "cfg-if",
723]
1399]
724
1400
1401+[[package]]
1402+name = "io-close"
1403+version = "0.3.7"
1404+source = "registry+https://github.com/rust-lang/crates.io-index"
1405+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
1406+dependencies = [
1407+ "libc",
1408+ "winapi",
1409+]
1410+
725[[package]]
1411[[package]]
726name = "itoa"
1412name = "itoa"
727version = "1.0.4"
1413version = "1.0.4"
@@ -815,6 +1501,21 @@ dependencies = [
815 "libc",
1501 "libc",
816]
1502]
817
1503
1504+[[package]]
1505+name = "minimal-lexical"
1506+version = "0.2.1"
1507+source = "registry+https://github.com/rust-lang/crates.io-index"
1508+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1509+
1510+[[package]]
1511+name = "miniz_oxide"
1512+version = "0.6.2"
1513+source = "registry+https://github.com/rust-lang/crates.io-index"
1514+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
1515+dependencies = [
1516+ "adler",
1517+]
1518+
818[[package]]
1519[[package]]
819name = "mio"
1520name = "mio"
820version = "0.8.5"
1521version = "0.8.5"
@@ -827,6 +1528,28 @@ dependencies = [
827 "windows-sys",
1528 "windows-sys",
828]
1529]
829
1530
1531+[[package]]
1532+name = "nix"
1533+version = "0.25.0"
1534+source = "registry+https://github.com/rust-lang/crates.io-index"
1535+checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
1536+dependencies = [
1537+ "autocfg",
1538+ "bitflags",
1539+ "cfg-if",
1540+ "libc",
1541+]
1542+
1543+[[package]]
1544+name = "nom"
1545+version = "7.1.1"
1546+source = "registry+https://github.com/rust-lang/crates.io-index"
1547+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
1548+dependencies = [
1549+ "memchr",
1550+ "minimal-lexical",
1551+]
1552+
830[[package]]
1553[[package]]
831name = "num-integer"
1554name = "num-integer"
832version = "0.1.45"
1555version = "0.1.45"
@@ -856,6 +1579,15 @@ dependencies = [
856 "libc",
1579 "libc",
857]
1580]
858
1581
1582+[[package]]
1583+name = "num_threads"
1584+version = "0.1.6"
1585+source = "registry+https://github.com/rust-lang/crates.io-index"
1586+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
1587+dependencies = [
1588+ "libc",
1589+]
1590+
859[[package]]
1591[[package]]
860name = "once_cell"
1592name = "once_cell"
861version = "1.16.0"
1593version = "1.16.0"
@@ -912,6 +1644,16 @@ dependencies = [
912 "unicode-ident",
1644 "unicode-ident",
913]
1645]
914
1646
1647+[[package]]
1648+name = "prodash"
1649+version = "21.1.0"
1650+source = "registry+https://github.com/rust-lang/crates.io-index"
1651+checksum = "7e13d7bd38cdab08b3a8b780cedcc54238c84fdca4084eb188807b308bcf11e6"
1652+dependencies = [
1653+ "bytesize",
1654+ "human_format",
1655+]
1656+
915[[package]]
1657[[package]]
916name = "pulldown-cmark"
1658name = "pulldown-cmark"
917version = "0.9.2"
1659version = "0.9.2"
@@ -923,6 +1665,12 @@ dependencies = [
923 "unicase",
1665 "unicase",
924]
1666]
925
1667
1668+[[package]]
1669+name = "quick-error"
1670+version = "2.0.1"
1671+source = "registry+https://github.com/rust-lang/crates.io-index"
1672+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
1673+
926[[package]]
1674[[package]]
927name = "quickcheck"
1675name = "quickcheck"
928version = "1.0.3"
1676version = "1.0.3"
@@ -1021,6 +1769,12 @@ dependencies = [
1021 "str_indices",
1769 "str_indices",
1022]
1770]
1023
1771
1772+[[package]]
1773+name = "rustversion"
1774+version = "1.0.9"
1775+source = "registry+https://github.com/rust-lang/crates.io-index"
1776+checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
1777+
1024[[package]]
1778[[package]]
1025name = "ryu"
1779name = "ryu"
1026version = "1.0.11"
1780version = "1.0.11"
@@ -1090,6 +1844,12 @@ dependencies = [
1090 "syn",
1844 "syn",
1091]
1845]
1092
1846
1847+[[package]]
1848+name = "sha1_smol"
1849+version = "1.0.0"
1850+source = "registry+https://github.com/rust-lang/crates.io-index"
1851+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
1852+
1093[[package]]
1853[[package]]
1094name = "signal-hook"
1854name = "signal-hook"
1095version = "0.3.14"
1855version = "0.3.14"
@@ -1293,6 +2053,35 @@ dependencies = [
1293 "num_cpus",
2053 "num_cpus",
1294]
2054]
1295
2055
2056+[[package]]
2057+name = "time"
2058+version = "0.3.17"
2059+source = "registry+https://github.com/rust-lang/crates.io-index"
2060+checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
2061+dependencies = [
2062+ "itoa",
2063+ "libc",
2064+ "num_threads",
2065+ "serde",
2066+ "time-core",
2067+ "time-macros",
2068+]
2069+
2070+[[package]]
2071+name = "time-core"
2072+version = "0.1.0"
2073+source = "registry+https://github.com/rust-lang/crates.io-index"
2074+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
2075+
2076+[[package]]
2077+name = "time-macros"
2078+version = "0.2.6"
2079+source = "registry+https://github.com/rust-lang/crates.io-index"
2080+checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
2081+dependencies = [
2082+ "time-core",
2083+]
2084+
1296[[package]]
2085[[package]]
1297name = "tinyvec"
2086name = "tinyvec"
1298version = "1.6.0"
2087version = "1.6.0"
@@ -1384,6 +2173,12 @@ version = "0.3.8"
1384source = "registry+https://github.com/rust-lang/crates.io-index"
2173source = "registry+https://github.com/rust-lang/crates.io-index"
1385checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
2174checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
1386
2175
2176+[[package]]
2177+name = "unicode-bom"
2178+version = "1.1.4"
2179+source = "registry+https://github.com/rust-lang/crates.io-index"
2180+checksum = "63ec69f541d875b783ca40184d655f2927c95f0bffd486faa83cd3ac3529ec32"
2181+
1387[[package]]
2182[[package]]
1388name = "unicode-general-category"
2183name = "unicode-general-category"
1389version = "0.6.0"
2184version = "0.6.0"
@@ -1439,6 +2234,12 @@ dependencies = [
1439 "serde",
2234 "serde",
1440]
2235]
1441
2236
2237+[[package]]
2238+name = "utf8-width"
2239+version = "0.1.6"
2240+source = "registry+https://github.com/rust-lang/crates.io-index"
2241+checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
2242+
1442[[package]]
2243[[package]]
1443name = "version_check"
2244name = "version_check"
1444version = "0.9.4"
2245version = "0.9.4"
@@ -1558,57 +2359,114 @@ version = "0.4.0"
1558source = "registry+https://github.com/rust-lang/crates.io-index"
2359source = "registry+https://github.com/rust-lang/crates.io-index"
1559checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
2360checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1560
2361
2362+[[package]]
2363+name = "windows"
2364+version = "0.40.0"
2365+source = "registry+https://github.com/rust-lang/crates.io-index"
2366+checksum = "e30acc718a52fb130fec72b1cb5f55ffeeec9253e1b785e94db222178a6acaa1"
2367+dependencies = [
2368+ "windows_aarch64_gnullvm 0.40.0",
2369+ "windows_aarch64_msvc 0.40.0",
2370+ "windows_i686_gnu 0.40.0",
2371+ "windows_i686_msvc 0.40.0",
2372+ "windows_x86_64_gnu 0.40.0",
2373+ "windows_x86_64_gnullvm 0.40.0",
2374+ "windows_x86_64_msvc 0.40.0",
2375+]
2376+
1561[[package]]
2377[[package]]
1562name = "windows-sys"
2378name = "windows-sys"
1563version = "0.42.0"
2379version = "0.42.0"
1564source = "registry+https://github.com/rust-lang/crates.io-index"
2380source = "registry+https://github.com/rust-lang/crates.io-index"
1565checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
2381checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
1566dependencies = [
2382dependencies = [
1567− "windows_aarch64_gnullvm",
2383+ "windows_aarch64_gnullvm 0.42.0",
1568− "windows_aarch64_msvc",
2384+ "windows_aarch64_msvc 0.42.0",
1569− "windows_i686_gnu",
2385+ "windows_i686_gnu 0.42.0",
1570− "windows_i686_msvc",
2386+ "windows_i686_msvc 0.42.0",
1571− "windows_x86_64_gnu",
2387+ "windows_x86_64_gnu 0.42.0",
1572− "windows_x86_64_gnullvm",
2388+ "windows_x86_64_gnullvm 0.42.0",
1573− "windows_x86_64_msvc",
2389+ "windows_x86_64_msvc 0.42.0",
1574]
2390]
1575
2391
2392+[[package]]
2393+name = "windows_aarch64_gnullvm"
2394+version = "0.40.0"
2395+source = "registry+https://github.com/rust-lang/crates.io-index"
2396+checksum = "f3caa4a1a16561b714323ca6b0817403738583033a6a92e04c5d10d4ba37ca10"
2397+
1576[[package]]
2398[[package]]
1577name = "windows_aarch64_gnullvm"
2399name = "windows_aarch64_gnullvm"
1578version = "0.42.0"
2400version = "0.42.0"
1579source = "registry+https://github.com/rust-lang/crates.io-index"
2401source = "registry+https://github.com/rust-lang/crates.io-index"
1580checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
2402checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
1581
2403
2404+[[package]]
2405+name = "windows_aarch64_msvc"
2406+version = "0.40.0"
2407+source = "registry+https://github.com/rust-lang/crates.io-index"
2408+checksum = "328973c62dfcc50fb1aaa8e7100676e0b642fe56bac6bafff3327902db843ab4"
2409+
1582[[package]]
2410[[package]]
1583name = "windows_aarch64_msvc"
2411name = "windows_aarch64_msvc"
1584version = "0.42.0"
2412version = "0.42.0"
1585source = "registry+https://github.com/rust-lang/crates.io-index"
2413source = "registry+https://github.com/rust-lang/crates.io-index"
1586checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
2414checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
1587
2415
2416+[[package]]
2417+name = "windows_i686_gnu"
2418+version = "0.40.0"
2419+source = "registry+https://github.com/rust-lang/crates.io-index"
2420+checksum = "aa5b09fad70f0df85dea2ac2a525537e415e2bf63ee31cf9b8e263645ee9f3c1"
2421+
1588[[package]]
2422[[package]]
1589name = "windows_i686_gnu"
2423name = "windows_i686_gnu"
1590version = "0.42.0"
2424version = "0.42.0"
1591source = "registry+https://github.com/rust-lang/crates.io-index"
2425source = "registry+https://github.com/rust-lang/crates.io-index"
1592checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
2426checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
1593
2427
2428+[[package]]
2429+name = "windows_i686_msvc"
2430+version = "0.40.0"
2431+source = "registry+https://github.com/rust-lang/crates.io-index"
2432+checksum = "2a1ad4031c1a98491fa195d8d43d7489cb749f135f2e5c4eed58da094bd0d876"
2433+
1594[[package]]
2434[[package]]
1595name = "windows_i686_msvc"
2435name = "windows_i686_msvc"
1596version = "0.42.0"
2436version = "0.42.0"
1597source = "registry+https://github.com/rust-lang/crates.io-index"
2437source = "registry+https://github.com/rust-lang/crates.io-index"
1598checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
2438checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
1599
2439
2440+[[package]]
2441+name = "windows_x86_64_gnu"
2442+version = "0.40.0"
2443+source = "registry+https://github.com/rust-lang/crates.io-index"
2444+checksum = "520ff37edd72da8064b49d2281182898e17f0688ae9f4070bca27e4b5c162ac7"
2445+
1600[[package]]
2446[[package]]
1601name = "windows_x86_64_gnu"
2447name = "windows_x86_64_gnu"
1602version = "0.42.0"
2448version = "0.42.0"
1603source = "registry+https://github.com/rust-lang/crates.io-index"
2449source = "registry+https://github.com/rust-lang/crates.io-index"
1604checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
2450checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
1605
2451
2452+[[package]]
2453+name = "windows_x86_64_gnullvm"
2454+version = "0.40.0"
2455+source = "registry+https://github.com/rust-lang/crates.io-index"
2456+checksum = "046e5b82215102c44fd75f488f1b9158973d02aa34d06ed85c23d6f5520a2853"
2457+
1606[[package]]
2458[[package]]
1607name = "windows_x86_64_gnullvm"
2459name = "windows_x86_64_gnullvm"
1608version = "0.42.0"
2460version = "0.42.0"
1609source = "registry+https://github.com/rust-lang/crates.io-index"
2461source = "registry+https://github.com/rust-lang/crates.io-index"
1610checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
2462checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
1611
2463
2464+[[package]]
2465+name = "windows_x86_64_msvc"
2466+version = "0.40.0"
2467+source = "registry+https://github.com/rust-lang/crates.io-index"
2468+checksum = "2a0c9c6df55dd1bfa76e131cef44bdd8ec9c819ef3611f04dfe453fd5bfeda28"
2469+
1612[[package]]
2470[[package]]
1613name = "windows_x86_64_msvc"
2471name = "windows_x86_64_msvc"
1614version = "0.42.0"
2472version = "0.42.0"
Cargo.toml+1
@@ -7,6 +7,7 @@ members = [
7 "helix-lsp",
7 "helix-lsp",
8 "helix-dap",
8 "helix-dap",
9 "helix-loader",
9 "helix-loader",
10+ "helix-vcs",
10 "xtask",
11 "xtask",
11]
12]
12
13
book/src/configuration.md+1−1
@@ -46,7 +46,7 @@ on unix operating systems.
46| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
46| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
47| `cursorline` | Highlight all lines with a cursor. | `false` |
47| `cursorline` | Highlight all lines with a cursor. | `false` |
48| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
48| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
49−| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers"]` |
49+| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
50| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
50| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
51| `auto-format` | Enable automatic formatting on save. | `true` |
51| `auto-format` | Enable automatic formatting on save. | `true` |
52| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
52| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
helix-core/src/lib.rs+1−1
@@ -83,7 +83,7 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::Path
83 top_marker.map_or(current_dir, |a| a.to_path_buf())
83 top_marker.map_or(current_dir, |a| a.to_path_buf())
84}
84}
85
85
86−pub use ropey::{str_utils, Rope, RopeBuilder, RopeSlice};
86+pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
87
87
88// pub use tendril::StrTendril as Tendril;
88// pub use tendril::StrTendril as Tendril;
89pub use smartstring::SmartString;
89pub use smartstring::SmartString;
helix-term/Cargo.toml+3
@@ -17,8 +17,10 @@ build = true
17app = true
17app = true
18
18
19[features]
19[features]
20+default = ["git"]
20unicode-lines = ["helix-core/unicode-lines"]
21unicode-lines = ["helix-core/unicode-lines"]
21integration = []
22integration = []
23+git = ["helix-vcs/git"]
22
24
23[[bin]]
25[[bin]]
24name = "hx"
26name = "hx"
@@ -29,6 +31,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
29helix-view = { version = "0.6", path = "../helix-view" }
31helix-view = { version = "0.6", path = "../helix-view" }
30helix-lsp = { version = "0.6", path = "../helix-lsp" }
32helix-lsp = { version = "0.6", path = "../helix-lsp" }
31helix-dap = { version = "0.6", path = "../helix-dap" }
33helix-dap = { version = "0.6", path = "../helix-dap" }
34+helix-vcs = { version = "0.6", path = "../helix-vcs" }
32helix-loader = { version = "0.6", path = "../helix-loader" }
35helix-loader = { version = "0.6", path = "../helix-loader" }
33
36
34anyhow = "1"
37anyhow = "1"
helix-term/src/application.rs+32−18
@@ -274,16 +274,27 @@ impl Application {
274 }
274 }
275
275
276 #[cfg(feature = "integration")]
276 #[cfg(feature = "integration")]
277− fn render(&mut self) {}
277+ async fn render(&mut self) {}
278
278
279 #[cfg(not(feature = "integration"))]
279 #[cfg(not(feature = "integration"))]
280− fn render(&mut self) {
280+ async fn render(&mut self) {
281 let mut cx = crate::compositor::Context {
281 let mut cx = crate::compositor::Context {
282 editor: &mut self.editor,
282 editor: &mut self.editor,
283 jobs: &mut self.jobs,
283 jobs: &mut self.jobs,
284 scroll: None,
284 scroll: None,
285 };
285 };
286
286
287+ // Acquire mutable access to the redraw_handle lock
288+ // to ensure that there are no tasks running that want to block rendering
289+ drop(cx.editor.redraw_handle.1.write().await);
290+ cx.editor.needs_redraw = false;
291+ {
292+ // exhaust any leftover redraw notifications
293+ let notify = cx.editor.redraw_handle.0.notified();
294+ tokio::pin!(notify);
295+ notify.enable();
296+ }
297+
287 let area = self
298 let area = self
288 .terminal
299 .terminal
289 .autoresize()
300 .autoresize()
@@ -304,7 +315,7 @@ impl Application {
304 where
315 where
305 S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
316 S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
306 {
317 {
307− self.render();
318+ self.render().await;
308 self.last_render = Instant::now();
319 self.last_render = Instant::now();
309
320
310 loop {
321 loop {
@@ -329,18 +340,18 @@ impl Application {
329 biased;
340 biased;
330
341
331 Some(event) = input_stream.next() => {
342 Some(event) = input_stream.next() => {
332− self.handle_terminal_events(event);
343+ self.handle_terminal_events(event).await;
333 }
344 }
334 Some(signal) = self.signals.next() => {
345 Some(signal) = self.signals.next() => {
335 self.handle_signals(signal).await;
346 self.handle_signals(signal).await;
336 }
347 }
337 Some(callback) = self.jobs.futures.next() => {
348 Some(callback) = self.jobs.futures.next() => {
338 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
349 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
339− self.render();
350+ self.render().await;
340 }
351 }
341 Some(callback) = self.jobs.wait_futures.next() => {
352 Some(callback) = self.jobs.wait_futures.next() => {
342 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
353 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
343− self.render();
354+ self.render().await;
344 }
355 }
345 event = self.editor.wait_event() => {
356 event = self.editor.wait_event() => {
346 let _idle_handled = self.handle_editor_event(event).await;
357 let _idle_handled = self.handle_editor_event(event).await;
@@ -445,25 +456,25 @@ impl Application {
445 self.compositor.resize(area);
456 self.compositor.resize(area);
446 self.terminal.clear().expect("couldn't clear terminal");
457 self.terminal.clear().expect("couldn't clear terminal");
447
458
448− self.render();
459+ self.render().await;
449 }
460 }
450 signal::SIGUSR1 => {
461 signal::SIGUSR1 => {
451 self.refresh_config();
462 self.refresh_config();
452− self.render();
463+ self.render().await;
453 }
464 }
454 _ => unreachable!(),
465 _ => unreachable!(),
455 }
466 }
456 }
467 }
457
468
458− pub fn handle_idle_timeout(&mut self) {
469+ pub async fn handle_idle_timeout(&mut self) {
459 let mut cx = crate::compositor::Context {
470 let mut cx = crate::compositor::Context {
460 editor: &mut self.editor,
471 editor: &mut self.editor,
461 jobs: &mut self.jobs,
472 jobs: &mut self.jobs,
462 scroll: None,
473 scroll: None,
463 };
474 };
464 let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
475 let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
465− if should_render {
476+ if should_render || self.editor.needs_redraw {
466− self.render();
477+ self.render().await;
467 }
478 }
468 }
479 }
469
480
@@ -536,11 +547,11 @@ impl Application {
536 match event {
547 match event {
537 EditorEvent::DocumentSaved(event) => {
548 EditorEvent::DocumentSaved(event) => {
538 self.handle_document_write(event);
549 self.handle_document_write(event);
539− self.render();
550+ self.render().await;
540 }
551 }
541 EditorEvent::ConfigEvent(event) => {
552 EditorEvent::ConfigEvent(event) => {
542 self.handle_config_events(event);
553 self.handle_config_events(event);
543− self.render();
554+ self.render().await;
544 }
555 }
545 EditorEvent::LanguageServerMessage((id, call)) => {
556 EditorEvent::LanguageServerMessage((id, call)) => {
546 self.handle_language_server_message(call, id).await;
557 self.handle_language_server_message(call, id).await;
@@ -548,19 +559,19 @@ impl Application {
548 let last = self.editor.language_servers.incoming.is_empty();
559 let last = self.editor.language_servers.incoming.is_empty();
549
560
550 if last || self.last_render.elapsed() > LSP_DEADLINE {
561 if last || self.last_render.elapsed() > LSP_DEADLINE {
551− self.render();
562+ self.render().await;
552 self.last_render = Instant::now();
563 self.last_render = Instant::now();
553 }
564 }
554 }
565 }
555 EditorEvent::DebuggerEvent(payload) => {
566 EditorEvent::DebuggerEvent(payload) => {
556 let needs_render = self.editor.handle_debugger_message(payload).await;
567 let needs_render = self.editor.handle_debugger_message(payload).await;
557 if needs_render {
568 if needs_render {
558− self.render();
569+ self.render().await;
559 }
570 }
560 }
571 }
561 EditorEvent::IdleTimer => {
572 EditorEvent::IdleTimer => {
562 self.editor.clear_idle_timer();
573 self.editor.clear_idle_timer();
563− self.handle_idle_timeout();
574+ self.handle_idle_timeout().await;
564
575
565 #[cfg(feature = "integration")]
576 #[cfg(feature = "integration")]
566 {
577 {
@@ -572,7 +583,10 @@ impl Application {
572 false
583 false
573 }
584 }
574
585
586+ pub async fn handle_terminal_events(
587+ &mut self,
575− pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
588+ event: Result<CrosstermEvent, crossterm::ErrorKind>,
589+ ) {
576 let mut cx = crate::compositor::Context {
590 let mut cx = crate::compositor::Context {
577 editor: &mut self.editor,
591 editor: &mut self.editor,
578 jobs: &mut self.jobs,
592 jobs: &mut self.jobs,
@@ -596,7 +610,7 @@ impl Application {
596 };
610 };
597
611
598 if should_redraw && !self.editor.should_close() {
612 if should_redraw && !self.editor.should_close() {
599− self.render();
613+ self.render().await;
600 }
614 }
601 }
615 }
602
616
helix-term/src/commands/typed.rs+7−4
@@ -1028,10 +1028,12 @@ fn reload(
1028 }
1028 }
1029
1029
1030 let scrolloff = cx.editor.config().scrolloff;
1030 let scrolloff = cx.editor.config().scrolloff;
1031+ let redraw_handle = cx.editor.redraw_handle.clone();
1031 let (view, doc) = current!(cx.editor);
1032 let (view, doc) = current!(cx.editor);
1032− doc.reload(view).map(|_| {
1033+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1034+ .map(|_| {
1033− view.ensure_cursor_in_view(doc, scrolloff);
1035+ view.ensure_cursor_in_view(doc, scrolloff);
1034− })
1036+ })
1035}
1037}
1036
1038
1037fn reload_all(
1039fn reload_all(
@@ -1066,7 +1068,8 @@ fn reload_all(
1066
1068
1067 // Every doc is guaranteed to have at least 1 view at this point.
1069 // Every doc is guaranteed to have at least 1 view at this point.
1068 let view = view_mut!(cx.editor, view_ids[0]);
1070 let view = view_mut!(cx.editor, view_ids[0]);
1071+ let redraw_handle = cx.editor.redraw_handle.clone();
1069− doc.reload(view)?;
1072+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
1070
1073
1071 for view_id in view_ids {
1074 for view_id in view_ids {
1072 let view = view_mut!(cx.editor, view_id);
1075 let view = view_mut!(cx.editor, view_id);
helix-term/src/ui/editor.rs+1−1
@@ -730,7 +730,7 @@ impl EditorView {
730 let mut text = String::with_capacity(8);
730 let mut text = String::with_capacity(8);
731
731
732 for gutter_type in view.gutters() {
732 for gutter_type in view.gutters() {
733− let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
733+ let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
734 let width = gutter_type.width(view, doc);
734 let width = gutter_type.width(view, doc);
735 text.reserve(width); // ensure there's enough space for the gutter
735 text.reserve(width); // ensure there's enough space for the gutter
736 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
736 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
helix-vcs/Cargo.tomlnew file+28
@@ -0,0 +1,28 @@
1+[package]
2+name = "helix-vcs"
3+version = "0.6.0"
4+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5+edition = "2021"
6+license = "MPL-2.0"
7+categories = ["editor"]
8+repository = "https://github.com/helix-editor/helix"
9+homepage = "https://helix-editor.com"
10+
11+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12+
13+[dependencies]
14+helix-core = { version = "0.6", path = "../helix-core" }
15+
16+tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
17+parking_lot = "0.12"
18+
19+git-repository = { version = "0.26", default-features = false , optional = true }
20+imara-diff = "0.1.5"
21+
22+log = "0.4"
23+
24+[features]
25+git = ["git-repository"]
26+
27+[dev-dependencies]
28+tempfile = "3.3"
helix-vcs/src/diff.rsnew file+198
@@ -0,0 +1,198 @@
1+use std::ops::Range;
2+use std::sync::Arc;
3+
4+use helix_core::Rope;
5+use imara_diff::Algorithm;
6+use parking_lot::{Mutex, MutexGuard};
7+use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8+use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
9+use tokio::task::JoinHandle;
10+use tokio::time::Instant;
11+
12+use crate::diff::worker::DiffWorker;
13+
14+mod line_cache;
15+mod worker;
16+
17+type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
18+
19+/// A rendering lock passed to the differ the prevents redraws from occurring
20+struct RenderLock {
21+ pub lock: OwnedRwLockReadGuard<()>,
22+ pub timeout: Option<Instant>,
23+}
24+
25+struct Event {
26+ text: Rope,
27+ is_base: bool,
28+ render_lock: Option<RenderLock>,
29+}
30+
31+#[derive(Clone, Debug)]
32+pub struct DiffHandle {
33+ channel: UnboundedSender<Event>,
34+ render_lock: Arc<RwLock<()>>,
35+ hunks: Arc<Mutex<Vec<Hunk>>>,
36+ inverted: bool,
37+}
38+
39+impl DiffHandle {
40+ pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
41+ DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
42+ }
43+
44+ fn new_with_handle(
45+ diff_base: Rope,
46+ doc: Rope,
47+ redraw_handle: RedrawHandle,
48+ ) -> (DiffHandle, JoinHandle<()>) {
49+ let (sender, receiver) = unbounded_channel();
50+ let hunks: Arc<Mutex<Vec<Hunk>>> = Arc::default();
51+ let worker = DiffWorker {
52+ channel: receiver,
53+ hunks: hunks.clone(),
54+ new_hunks: Vec::default(),
55+ redraw_notify: redraw_handle.0,
56+ diff_finished_notify: Arc::default(),
57+ };
58+ let handle = tokio::spawn(worker.run(diff_base, doc));
59+ let differ = DiffHandle {
60+ channel: sender,
61+ hunks,
62+ inverted: false,
63+ render_lock: redraw_handle.1,
64+ };
65+ (differ, handle)
66+ }
67+
68+ pub fn invert(&mut self) {
69+ self.inverted = !self.inverted;
70+ }
71+
72+ pub fn hunks(&self) -> FileHunks {
73+ FileHunks {
74+ hunks: self.hunks.lock(),
75+ inverted: self.inverted,
76+ }
77+ }
78+
79+ /// Updates the document associated with this redraw handle
80+ /// This function is only intended to be called from within the rendering loop
81+ /// if called from elsewhere it may fail to acquire the render lock and panic
82+ pub fn update_document(&self, doc: Rope, block: bool) -> bool {
83+ // unwrap is ok here because the rendering lock is
84+ // only exclusively locked during redraw.
85+ // This function is only intended to be called
86+ // from the core rendering loop where no redraw can happen in parallel
87+ let lock = self.render_lock.clone().try_read_owned().unwrap();
88+ let timeout = if block {
89+ None
90+ } else {
91+ Some(Instant::now() + tokio::time::Duration::from_millis(SYNC_DIFF_TIMEOUT))
92+ };
93+ self.update_document_impl(doc, self.inverted, Some(RenderLock { lock, timeout }))
94+ }
95+
96+ pub fn update_diff_base(&self, diff_base: Rope) -> bool {
97+ self.update_document_impl(diff_base, !self.inverted, None)
98+ }
99+
100+ fn update_document_impl(
101+ &self,
102+ text: Rope,
103+ is_base: bool,
104+ render_lock: Option<RenderLock>,
105+ ) -> bool {
106+ let event = Event {
107+ text,
108+ is_base,
109+ render_lock,
110+ };
111+ self.channel.send(event).is_ok()
112+ }
113+}
114+
115+/// synchronous debounce value should be low
116+/// so we can update synchronously most of the time
117+const DIFF_DEBOUNCE_TIME_SYNC: u64 = 1;
118+/// maximum time that rendering should be blocked until the diff finishes
119+const SYNC_DIFF_TIMEOUT: u64 = 12;
120+const DIFF_DEBOUNCE_TIME_ASYNC: u64 = 96;
121+const ALGORITHM: Algorithm = Algorithm::Histogram;
122+const MAX_DIFF_LINES: usize = 64 * u16::MAX as usize;
123+// cap average line length to 128 for files with MAX_DIFF_LINES
124+const MAX_DIFF_BYTES: usize = MAX_DIFF_LINES * 128;
125+
126+/// A single change in a file potentially spanning multiple lines
127+/// Hunks produced by the differs are always ordered by their position
128+/// in the file and non-overlapping.
129+/// Specifically for any two hunks `x` and `y` the following properties hold:
130+///
131+/// ``` no_compile
132+/// assert!(x.before.end <= y.before.start);
133+/// assert!(x.after.end <= y.after.start);
134+/// ```
135+#[derive(PartialEq, Eq, Clone, Debug)]
136+pub struct Hunk {
137+ pub before: Range<u32>,
138+ pub after: Range<u32>,
139+}
140+
141+impl Hunk {
142+ /// Can be used instead of `Option::None` for better performance
143+ /// because lines larger then `i32::MAX` are not supported by `imara-diff` anyways.
144+ /// Has some nice properties where it usually is not necessary to check for `None` separately:
145+ /// Empty ranges fail contains checks and also fails smaller then checks.
146+ pub const NONE: Hunk = Hunk {
147+ before: u32::MAX..u32::MAX,
148+ after: u32::MAX..u32::MAX,
149+ };
150+
151+ /// Inverts a change so that `before`
152+ pub fn invert(&self) -> Hunk {
153+ Hunk {
154+ before: self.after.clone(),
155+ after: self.before.clone(),
156+ }
157+ }
158+
159+ pub fn is_pure_insertion(&self) -> bool {
160+ self.before.is_empty()
161+ }
162+
163+ pub fn is_pure_removal(&self) -> bool {
164+ self.after.is_empty()
165+ }
166+}
167+
168+/// A list of changes in a file sorted in ascending
169+/// non-overlapping order
170+#[derive(Debug)]
171+pub struct FileHunks<'a> {
172+ hunks: MutexGuard<'a, Vec<Hunk>>,
173+ inverted: bool,
174+}
175+
176+impl FileHunks<'_> {
177+ pub fn is_inverted(&self) -> bool {
178+ self.inverted
179+ }
180+
181+ /// Returns the `Hunk` for the `n`th change in this file.
182+ /// if there is no `n`th change `Hunk::NONE` is returned instead.
183+ pub fn nth_hunk(&self, n: u32) -> Hunk {
184+ match self.hunks.get(n as usize) {
185+ Some(hunk) if self.inverted => hunk.invert(),
186+ Some(hunk) => hunk.clone(),
187+ None => Hunk::NONE,
188+ }
189+ }
190+
191+ pub fn len(&self) -> u32 {
192+ self.hunks.len() as u32
193+ }
194+
195+ pub fn is_empty(&self) -> bool {
196+ self.len() == 0
197+ }
198+}
helix-vcs/src/diff/line_cache.rsnew file+130
@@ -0,0 +1,130 @@
1+//! This modules encapsulates a tiny bit of unsafe code that
2+//! makes diffing significantly faster and more ergonomic to implement.
3+//! This code is necessary because diffing requires quick random
4+//! access to the lines of the text that is being diffed.
5+//!
6+//! Therefore it is best to collect the `Rope::lines` iterator into a vec
7+//! first because access to the vec is `O(1)` where `Rope::line` is `O(log N)`.
8+//! However this process can allocate a (potentially quite large) vector.
9+//!
10+//! To avoid reallocation for every diff, the vector is reused.
11+//! However the RopeSlice references the original rope and therefore forms a self-referential data structure.
12+//! A transmute is used to change the lifetime of the slice to static to circumvent that project.
13+use std::mem::transmute;
14+
15+use helix_core::{Rope, RopeSlice};
16+use imara_diff::intern::{InternedInput, Interner};
17+
18+use super::{MAX_DIFF_BYTES, MAX_DIFF_LINES};
19+
20+/// A cache that stores the `lines` of a rope as a vector.
21+/// It allows safely reusing the allocation of the vec when updating the rope
22+pub(crate) struct InternedRopeLines {
23+ diff_base: Rope,
24+ doc: Rope,
25+ num_tokens_diff_base: u32,
26+ interned: InternedInput<RopeSlice<'static>>,
27+}
28+
29+impl InternedRopeLines {
30+ pub fn new(diff_base: Rope, doc: Rope) -> InternedRopeLines {
31+ let mut res = InternedRopeLines {
32+ interned: InternedInput {
33+ before: Vec::with_capacity(diff_base.len_lines()),
34+ after: Vec::with_capacity(doc.len_lines()),
35+ interner: Interner::new(diff_base.len_lines() + doc.len_lines()),
36+ },
37+ diff_base,
38+ doc,
39+ // will be populated by update_diff_base_impl
40+ num_tokens_diff_base: 0,
41+ };
42+ res.update_diff_base_impl();
43+ res
44+ }
45+
46+ /// Updates the `diff_base` and optionally the document if `doc` is not None
47+ pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option<Rope>) {
48+ self.interned.clear();
49+ self.diff_base = diff_base;
50+ if let Some(doc) = doc {
51+ self.doc = doc
52+ }
53+ if !self.is_too_large() {
54+ self.update_diff_base_impl();
55+ }
56+ }
57+
58+ /// Updates the `doc` without reinterning the `diff_base`, this function
59+ /// is therefore significantly faster than `update_diff_base` when only the document changes.
60+ pub fn update_doc(&mut self, doc: Rope) {
61+ // Safety: we clear any tokens that were added after
62+ // the interning of `self.diff_base` finished so
63+ // all lines that refer to `self.doc` have been purged.
64+
65+ self.interned
66+ .interner
67+ .erase_tokens_after(self.num_tokens_diff_base.into());
68+
69+ self.doc = doc;
70+ if self.is_too_large() {
71+ self.interned.after.clear();
72+ } else {
73+ self.update_doc_impl();
74+ }
75+ }
76+
77+ fn update_diff_base_impl(&mut self) {
78+ // Safety: This transmute is safe because it only transmutes a lifetime, which has no effect.
79+ // The backing storage for the RopeSlices referred to by the lifetime is stored in `self.diff_base`.
80+ // Therefore as long as `self.diff_base` is not dropped/replaced this memory remains valid.
81+ // `self.diff_base` is only changed in `self.update_diff_base`, which clears the interner.
82+ // When the interned lines are exposed to consumer in `self.diff_input`, the lifetime is bounded to a reference to self.
83+ // That means that on calls to update there exist no references to `self.interned`.
84+ let before = self
85+ .diff_base
86+ .lines()
87+ .map(|line: RopeSlice| -> RopeSlice<'static> { unsafe { transmute(line) } });
88+ self.interned.update_before(before);
89+ self.num_tokens_diff_base = self.interned.interner.num_tokens();
90+ // the has to be interned again because the interner was fully cleared
91+ self.update_doc_impl()
92+ }
93+
94+ fn update_doc_impl(&mut self) {
95+ // Safety: This transmute is save because it only transmutes a lifetime, which has no effect.
96+ // The backing storage for the RopeSlices referred to by the lifetime is stored in `self.doc`.
97+ // Therefore as long as `self.doc` is not dropped/replaced this memory remains valid.
98+ // `self.doc` is only changed in `self.update_doc`, which clears the interner.
99+ // When the interned lines are exposed to consumer in `self.diff_input`, the lifetime is bounded to a reference to self.
100+ // That means that on calls to update there exist no references to `self.interned`.
101+ let after = self
102+ .doc
103+ .lines()
104+ .map(|line: RopeSlice| -> RopeSlice<'static> { unsafe { transmute(line) } });
105+ self.interned.update_after(after);
106+ }
107+
108+ fn is_too_large(&self) -> bool {
109+ // bound both lines and bytes to avoid huge files with few (but huge) lines
110+ // or huge file with tiny lines. While this makes no difference to
111+ // diff itself (the diff performance only depends on the number of tokens)
112+ // the interning runtime depends mostly on filesize and is actually dominant
113+ // for large files
114+ self.doc.len_lines() > MAX_DIFF_LINES
115+ || self.diff_base.len_lines() > MAX_DIFF_LINES
116+ || self.doc.len_bytes() > MAX_DIFF_BYTES
117+ || self.diff_base.len_bytes() > MAX_DIFF_BYTES
118+ }
119+
120+ /// Returns the `InternedInput` for performing the diff.
121+ /// If `diff_base` or `doc` is so large that performing a diff could slow the editor
122+ /// this function returns `None`.
123+ pub fn interned_lines(&self) -> Option<&InternedInput<RopeSlice>> {
124+ if self.is_too_large() {
125+ None
126+ } else {
127+ Some(&self.interned)
128+ }
129+ }
130+}
helix-vcs/src/diff/worker.rsnew file+207
@@ -0,0 +1,207 @@
1+use std::mem::swap;
2+use std::ops::Range;
3+use std::sync::Arc;
4+
5+use helix_core::{Rope, RopeSlice};
6+use imara_diff::intern::InternedInput;
7+use parking_lot::Mutex;
8+use tokio::sync::mpsc::UnboundedReceiver;
9+use tokio::sync::Notify;
10+use tokio::time::{timeout, timeout_at, Duration};
11+
12+use crate::diff::{
13+ Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC,
14+};
15+
16+use super::line_cache::InternedRopeLines;
17+use super::Hunk;
18+
19+#[cfg(test)]
20+mod test;
21+
22+pub(super) struct DiffWorker {
23+ pub channel: UnboundedReceiver<Event>,
24+ pub hunks: Arc<Mutex<Vec<Hunk>>>,
25+ pub new_hunks: Vec<Hunk>,
26+ pub redraw_notify: Arc<Notify>,
27+ pub diff_finished_notify: Arc<Notify>,
28+}
29+
30+impl DiffWorker {
31+ async fn accumulate_events(&mut self, event: Event) -> (Option<Rope>, Option<Rope>) {
32+ let mut accumulator = EventAccumulator::new();
33+ accumulator.handle_event(event).await;
34+ accumulator
35+ .accumulate_debounced_events(
36+ &mut self.channel,
37+ self.redraw_notify.clone(),
38+ self.diff_finished_notify.clone(),
39+ )
40+ .await;
41+ (accumulator.doc, accumulator.diff_base)
42+ }
43+
44+ pub async fn run(mut self, diff_base: Rope, doc: Rope) {
45+ let mut interner = InternedRopeLines::new(diff_base, doc);
46+ if let Some(lines) = interner.interned_lines() {
47+ self.perform_diff(lines);
48+ }
49+ self.apply_hunks();
50+ while let Some(event) = self.channel.recv().await {
51+ let (doc, diff_base) = self.accumulate_events(event).await;
52+
53+ let process_accumulated_events = || {
54+ if let Some(new_base) = diff_base {
55+ interner.update_diff_base(new_base, doc)
56+ } else {
57+ interner.update_doc(doc.unwrap())
58+ }
59+
60+ if let Some(lines) = interner.interned_lines() {
61+ self.perform_diff(lines)
62+ }
63+ };
64+
65+ // Calculating diffs is computationally expensive and should
66+ // not run inside an async function to avoid blocking other futures.
67+ // Note: tokio::task::block_in_place does not work during tests
68+ #[cfg(test)]
69+ process_accumulated_events();
70+ #[cfg(not(test))]
71+ tokio::task::block_in_place(process_accumulated_events);
72+
73+ self.apply_hunks();
74+ }
75+ }
76+
77+ /// update the hunks (used by the gutter) by replacing it with `self.new_hunks`.
78+ /// `self.new_hunks` is always empty after this function runs.
79+ /// To improve performance this function tries to reuse the allocation of the old diff previously stored in `self.line_diffs`
80+ fn apply_hunks(&mut self) {
81+ swap(&mut *self.hunks.lock(), &mut self.new_hunks);
82+ self.diff_finished_notify.notify_waiters();
83+ self.new_hunks.clear();
84+ }
85+
86+ fn perform_diff(&mut self, input: &InternedInput<RopeSlice>) {
87+ imara_diff::diff(ALGORITHM, input, |before: Range<u32>, after: Range<u32>| {
88+ self.new_hunks.push(Hunk { before, after })
89+ })
90+ }
91+}
92+
93+struct EventAccumulator {
94+ diff_base: Option<Rope>,
95+ doc: Option<Rope>,
96+ render_lock: Option<RenderLock>,
97+}
98+
99+impl<'a> EventAccumulator {
100+ fn new() -> EventAccumulator {
101+ EventAccumulator {
102+ diff_base: None,
103+ doc: None,
104+ render_lock: None,
105+ }
106+ }
107+
108+ async fn handle_event(&mut self, event: Event) {
109+ let dst = if event.is_base {
110+ &mut self.diff_base
111+ } else {
112+ &mut self.doc
113+ };
114+
115+ *dst = Some(event.text);
116+
117+ // always prefer the most synchronous requested render mode
118+ if let Some(render_lock) = event.render_lock {
119+ match &mut self.render_lock {
120+ Some(RenderLock { timeout, .. }) => {
121+ // A timeout of `None` means that the render should
122+ // always wait for the diff to complete (so no timeout)
123+ // remove the existing timeout, otherwise keep the previous timeout
124+ // because it will be shorter then the current timeout
125+ if render_lock.timeout.is_none() {
126+ timeout.take();
127+ }
128+ }
129+ None => self.render_lock = Some(render_lock),
130+ }
131+ }
132+ }
133+
134+ async fn accumulate_debounced_events(
135+ &mut self,
136+ channel: &mut UnboundedReceiver<Event>,
137+ redraw_notify: Arc<Notify>,
138+ diff_finished_notify: Arc<Notify>,
139+ ) {
140+ let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
141+ let sync_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_SYNC);
142+ loop {
143+ // if we are not blocking rendering use a much longer timeout
144+ let debounce = if self.render_lock.is_none() {
145+ async_debounce
146+ } else {
147+ sync_debounce
148+ };
149+
150+ if let Ok(Some(event)) = timeout(debounce, channel.recv()).await {
151+ self.handle_event(event).await;
152+ } else {
153+ break;
154+ }
155+ }
156+
157+ // setup task to trigger the rendering
158+ match self.render_lock.take() {
159+ // diff is performed outside of the rendering loop
160+ // request a redraw after the diff is done
161+ None => {
162+ tokio::spawn(async move {
163+ diff_finished_notify.notified().await;
164+ redraw_notify.notify_one();
165+ });
166+ }
167+ // diff is performed inside the rendering loop
168+ // block redraw until the diff is done or the timeout is expired
169+ Some(RenderLock {
170+ lock,
171+ timeout: Some(timeout),
172+ }) => {
173+ tokio::spawn(async move {
174+ let res = {
175+ // Acquire a lock on the redraw handle.
176+ // The lock will block the rendering from occurring while held.
177+ // The rendering waits for the diff if it doesn't time out
178+ timeout_at(timeout, diff_finished_notify.notified()).await
179+ };
180+ // we either reached the timeout or the diff is finished, release the render lock
181+ drop(lock);
182+ if res.is_ok() {
183+ // Diff finished in time we are done.
184+ return;
185+ }
186+ // Diff failed to complete in time log the event
187+ // and wait until the diff occurs to trigger an async redraw
188+ log::warn!("Diff computation timed out, update of diffs might appear delayed");
189+ diff_finished_notify.notified().await;
190+ redraw_notify.notify_one();
191+ });
192+ }
193+ // a blocking diff is performed inside the rendering loop
194+ // block redraw until the diff is done
195+ Some(RenderLock {
196+ lock,
197+ timeout: None,
198+ }) => {
199+ tokio::spawn(async move {
200+ diff_finished_notify.notified().await;
201+ // diff is done release the lock
202+ drop(lock)
203+ });
204+ }
205+ };
206+ }
207+}
helix-vcs/src/diff/worker/test.rsnew file+149
@@ -0,0 +1,149 @@
1+use helix_core::Rope;
2+use tokio::task::JoinHandle;
3+
4+use crate::diff::{DiffHandle, Hunk};
5+
6+impl DiffHandle {
7+ fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
8+ DiffHandle::new_with_handle(
9+ Rope::from_str(diff_base),
10+ Rope::from_str(doc),
11+ Default::default(),
12+ )
13+ }
14+ async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
15+ let hunks = self.hunks;
16+ // dropping the channel terminates the task
17+ drop(self.channel);
18+ handle.await.unwrap();
19+ let hunks = hunks.lock();
20+ Vec::clone(&*hunks)
21+ }
22+}
23+
24+#[tokio::test]
25+async fn append_line() {
26+ let (differ, handle) = DiffHandle::new_test("foo\n", "foo\nbar\n");
27+ let line_diffs = differ.into_diff(handle).await;
28+ assert_eq!(
29+ &line_diffs,
30+ &[Hunk {
31+ before: 1..1,
32+ after: 1..2
33+ }]
34+ )
35+}
36+
37+#[tokio::test]
38+async fn prepend_line() {
39+ let (differ, handle) = DiffHandle::new_test("foo\n", "bar\nfoo\n");
40+ let line_diffs = differ.into_diff(handle).await;
41+ assert_eq!(
42+ &line_diffs,
43+ &[Hunk {
44+ before: 0..0,
45+ after: 0..1
46+ }]
47+ )
48+}
49+
50+#[tokio::test]
51+async fn modify() {
52+ let (differ, handle) = DiffHandle::new_test("foo\nbar\n", "foo bar\nbar\n");
53+ let line_diffs = differ.into_diff(handle).await;
54+ assert_eq!(
55+ &line_diffs,
56+ &[Hunk {
57+ before: 0..1,
58+ after: 0..1
59+ }]
60+ )
61+}
62+
63+#[tokio::test]
64+async fn delete_line() {
65+ let (differ, handle) = DiffHandle::new_test("foo\nfoo bar\nbar\n", "foo\nbar\n");
66+ let line_diffs = differ.into_diff(handle).await;
67+ assert_eq!(
68+ &line_diffs,
69+ &[Hunk {
70+ before: 1..2,
71+ after: 1..1
72+ }]
73+ )
74+}
75+
76+#[tokio::test]
77+async fn delete_line_and_modify() {
78+ let (differ, handle) = DiffHandle::new_test("foo\nbar\ntest\nfoo", "foo\ntest\nfoo bar");
79+ let line_diffs = differ.into_diff(handle).await;
80+ assert_eq!(
81+ &line_diffs,
82+ &[
83+ Hunk {
84+ before: 1..2,
85+ after: 1..1
86+ },
87+ Hunk {
88+ before: 3..4,
89+ after: 2..3
90+ },
91+ ]
92+ )
93+}
94+
95+#[tokio::test]
96+async fn add_use() {
97+ let (differ, handle) = DiffHandle::new_test(
98+ "use ropey::Rope;\nuse tokio::task::JoinHandle;\n",
99+ "use ropey::Rope;\nuse ropey::RopeSlice;\nuse tokio::task::JoinHandle;\n",
100+ );
101+ let line_diffs = differ.into_diff(handle).await;
102+ assert_eq!(
103+ &line_diffs,
104+ &[Hunk {
105+ before: 1..1,
106+ after: 1..2
107+ },]
108+ )
109+}
110+
111+#[tokio::test]
112+async fn update_document() {
113+ let (differ, handle) = DiffHandle::new_test("foo\nbar\ntest\nfoo", "foo\nbar\ntest\nfoo");
114+ differ.update_document(Rope::from_str("foo\ntest\nfoo bar"), false);
115+ let line_diffs = differ.into_diff(handle).await;
116+ assert_eq!(
117+ &line_diffs,
118+ &[
119+ Hunk {
120+ before: 1..2,
121+ after: 1..1
122+ },
123+ Hunk {
124+ before: 3..4,
125+ after: 2..3
126+ },
127+ ]
128+ )
129+}
130+
131+#[tokio::test]
132+async fn update_base() {
133+ let (differ, handle) = DiffHandle::new_test("foo\ntest\nfoo bar", "foo\ntest\nfoo bar");
134+ differ.update_diff_base(Rope::from_str("foo\nbar\ntest\nfoo"));
135+ let line_diffs = differ.into_diff(handle).await;
136+ assert_eq!(
137+ &line_diffs,
138+ &[
139+ Hunk {
140+ before: 1..2,
141+ after: 1..1
142+ },
143+ Hunk {
144+ before: 3..4,
145+ after: 2..3
146+ },
147+ ]
148+ )
149+}
helix-vcs/src/git.rsnew file+80
@@ -0,0 +1,80 @@
1+use std::path::Path;
2+
3+use git::objs::tree::EntryMode;
4+use git::sec::trust::DefaultForLevel;
5+use git::{Commit, ObjectId, Repository, ThreadSafeRepository};
6+use git_repository as git;
7+
8+use crate::DiffProvider;
9+
10+#[cfg(test)]
11+mod test;
12+
13+pub struct Git;
14+
15+impl Git {
16+ fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option<ThreadSafeRepository> {
17+ // custom open options
18+ let mut git_open_opts_map = git::sec::trust::Mapping::<git::open::Options>::default();
19+
20+ // don't use the global git configs (not needed)
21+ let config = git::permissions::Config {
22+ system: false,
23+ git: false,
24+ user: false,
25+ env: true,
26+ includes: true,
27+ git_binary: false,
28+ };
29+ // change options for config permissions without touching anything else
30+ git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(git::Permissions {
31+ config,
32+ ..git::Permissions::default_for_level(git::sec::Trust::Reduced)
33+ });
34+ git_open_opts_map.full = git_open_opts_map.full.permissions(git::Permissions {
35+ config,
36+ ..git::Permissions::default_for_level(git::sec::Trust::Full)
37+ });
38+
39+ let mut open_options = git::discover::upwards::Options::default();
40+ if let Some(ceiling_dir) = ceiling_dir {
41+ open_options.ceiling_dirs = vec![ceiling_dir.to_owned()];
42+ }
43+
44+ ThreadSafeRepository::discover_with_environment_overrides_opts(
45+ path,
46+ open_options,
47+ git_open_opts_map,
48+ )
49+ .ok()
50+ }
51+}
52+
53+impl DiffProvider for Git {
54+ fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
55+ debug_assert!(!file.exists() || file.is_file());
56+ debug_assert!(file.is_absolute());
57+
58+ // TODO cache repository lookup
59+ let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
60+ let head = repo.head_commit().ok()?;
61+ let file_oid = find_file_in_commit(&repo, &head, file)?;
62+
63+ let file_object = repo.find_object(file_oid).ok()?;
64+ Some(file_object.detach().data)
65+ }
66+}
67+
68+/// Finds the object that contains the contents of a file at a specific commit.
69+fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option<ObjectId> {
70+ let repo_dir = repo.work_dir()?;
71+ let rel_path = file.strip_prefix(repo_dir).ok()?;
72+ let tree = commit.tree().ok()?;
73+ let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??;
74+ match tree_entry.mode() {
75+ // not a file, everything is new, do not show diff
76+ EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None,
77+ // found a file
78+ EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()),
79+ }
80+}
helix-vcs/src/git/test.rsnew file+121
@@ -0,0 +1,121 @@
1+use std::{fs::File, io::Write, path::Path, process::Command};
2+
3+use tempfile::TempDir;
4+
5+use crate::{DiffProvider, Git};
6+
7+fn exec_git_cmd(args: &str, git_dir: &Path) {
8+ let res = Command::new("git")
9+ .arg("-C")
10+ .arg(git_dir) // execute the git command in this directory
11+ .args(args.split_whitespace())
12+ .env_remove("GIT_DIR")
13+ .env_remove("GIT_ASKPASS")
14+ .env_remove("SSH_ASKPASS")
15+ .env("GIT_TERMINAL_PROMPT", "false")
16+ .env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
17+ .env("GIT_AUTHOR_EMAIL", "author@example.com")
18+ .env("GIT_AUTHOR_NAME", "author")
19+ .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
20+ .env("GIT_COMMITTER_EMAIL", "committer@example.com")
21+ .env("GIT_COMMITTER_NAME", "committer")
22+ .env("GIT_CONFIG_COUNT", "2")
23+ .env("GIT_CONFIG_KEY_0", "commit.gpgsign")
24+ .env("GIT_CONFIG_VALUE_0", "false")
25+ .env("GIT_CONFIG_KEY_1", "init.defaultBranch")
26+ .env("GIT_CONFIG_VALUE_1", "main")
27+ .output()
28+ .unwrap_or_else(|_| panic!("`git {args}` failed"));
29+ if !res.status.success() {
30+ println!("{}", String::from_utf8_lossy(&res.stdout));
31+ eprintln!("{}", String::from_utf8_lossy(&res.stderr));
32+ panic!("`git {args}` failed (see output above)")
33+ }
34+}
35+
36+fn create_commit(repo: &Path, add_modified: bool) {
37+ if add_modified {
38+ exec_git_cmd("add -A", repo);
39+ }
40+ exec_git_cmd("commit -m message", repo);
41+}
42+
43+fn empty_git_repo() -> TempDir {
44+ let tmp = tempfile::tempdir().expect("create temp dir for git testing");
45+ exec_git_cmd("init", tmp.path());
46+ exec_git_cmd("config user.email test@helix.org", tmp.path());
47+ exec_git_cmd("config user.name helix-test", tmp.path());
48+ tmp
49+}
50+
51+#[test]
52+fn missing_file() {
53+ let temp_git = empty_git_repo();
54+ let file = temp_git.path().join("file.txt");
55+ File::create(&file).unwrap().write_all(b"foo").unwrap();
56+
57+ assert_eq!(Git.get_diff_base(&file), None);
58+}
59+
60+#[test]
61+fn unmodified_file() {
62+ let temp_git = empty_git_repo();
63+ let file = temp_git.path().join("file.txt");
64+ let contents = b"foo".as_slice();
65+ File::create(&file).unwrap().write_all(contents).unwrap();
66+ create_commit(temp_git.path(), true);
67+ assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
68+}
69+
70+#[test]
71+fn modified_file() {
72+ let temp_git = empty_git_repo();
73+ let file = temp_git.path().join("file.txt");
74+ let contents = b"foo".as_slice();
75+ File::create(&file).unwrap().write_all(contents).unwrap();
76+ create_commit(temp_git.path(), true);
77+ File::create(&file).unwrap().write_all(b"bar").unwrap();
78+
79+ assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
80+}
81+
82+/// Test that `get_file_head` does not return content for a directory.
83+/// This is important to correctly cover cases where a directory is removed and replaced by a file.
84+/// If the contents of the directory object were returned a diff between a path and the directory children would be produced.
85+#[test]
86+fn directory() {
87+ let temp_git = empty_git_repo();
88+ let dir = temp_git.path().join("file.txt");
89+ std::fs::create_dir(&dir).expect("");
90+ let file = dir.join("file.txt");
91+ let contents = b"foo".as_slice();
92+ File::create(&file).unwrap().write_all(contents).unwrap();
93+
94+ create_commit(temp_git.path(), true);
95+
96+ std::fs::remove_dir_all(&dir).unwrap();
97+ File::create(&dir).unwrap().write_all(b"bar").unwrap();
98+ assert_eq!(Git.get_diff_base(&dir), None);
99+}
100+
101+/// Test that `get_file_head` does not return content for a symlink.
102+/// This is important to correctly cover cases where a symlink is removed and replaced by a file.
103+/// If the contents of the symlink object were returned a diff between a path and the actual file would be produced (bad ui).
104+#[cfg(any(unix, windows))]
105+#[test]
106+fn symlink() {
107+ #[cfg(unix)]
108+ use std::os::unix::fs::symlink;
109+ #[cfg(not(unix))]
110+ use std::os::windows::fs::symlink_file as symlink;
111+ let temp_git = empty_git_repo();
112+ let file = temp_git.path().join("file.txt");
113+ let contents = b"foo".as_slice();
114+ File::create(&file).unwrap().write_all(contents).unwrap();
115+ let file_link = temp_git.path().join("file_link.txt");
116+ symlink("file.txt", &file_link).unwrap();
117+
118+ create_commit(temp_git.path(), true);
119+ assert_eq!(Git.get_diff_base(&file_link), None);
120+ assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
121+}
helix-vcs/src/lib.rsnew file+51
@@ -0,0 +1,51 @@
1+use std::path::Path;
2+
3+#[cfg(feature = "git")]
4+pub use git::Git;
5+#[cfg(not(feature = "git"))]
6+pub use Dummy as Git;
7+
8+#[cfg(feature = "git")]
9+mod git;
10+
11+mod diff;
12+
13+pub use diff::{DiffHandle, Hunk};
14+
15+pub trait DiffProvider {
16+ /// Returns the data that a diff should be computed against
17+ /// if this provider is used.
18+ /// The data is returned as raw byte without any decoding or encoding performed
19+ /// to ensure all file encodings are handled correctly.
20+ fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>>;
21+}
22+
23+#[doc(hidden)]
24+pub struct Dummy;
25+impl DiffProvider for Dummy {
26+ fn get_diff_base(&self, _file: &Path) -> Option<Vec<u8>> {
27+ None
28+ }
29+}
30+
31+pub struct DiffProviderRegistry {
32+ providers: Vec<Box<dyn DiffProvider>>,
33+}
34+
35+impl DiffProviderRegistry {
36+ pub fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
37+ self.providers
38+ .iter()
39+ .find_map(|provider| provider.get_diff_base(file))
40+ }
41+}
42+
43+impl Default for DiffProviderRegistry {
44+ fn default() -> Self {
45+ // currently only git is supported
46+ // TODO make this configurable when more providers are added
47+ let git: Box<dyn DiffProvider> = Box::new(Git);
48+ let providers = vec![git];
49+ DiffProviderRegistry { providers }
50+ }
51+}
helix-view/Cargo.toml+2
@@ -21,6 +21,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" }
21helix-lsp = { version = "0.6", path = "../helix-lsp" }
21helix-lsp = { version = "0.6", path = "../helix-lsp" }
22helix-dap = { version = "0.6", path = "../helix-dap" }
22helix-dap = { version = "0.6", path = "../helix-dap" }
23crossterm = { version = "0.25", optional = true }
23crossterm = { version = "0.25", optional = true }
24+helix-vcs = { version = "0.6", path = "../helix-vcs" }
24
25
25# Conversion traits
26# Conversion traits
26once_cell = "1.16"
27once_cell = "1.16"
@@ -43,6 +44,7 @@ log = "~0.4"
43
44
44which = "4.2"
45which = "4.2"
45
46
47+
46[target.'cfg(windows)'.dependencies]
48[target.'cfg(windows)'.dependencies]
47clipboard-win = { version = "4.4", features = ["std"] }
49clipboard-win = { version = "4.4", features = ["std"] }
48
50
helix-view/src/document.rs+44−8
@@ -3,6 +3,8 @@ use futures_util::future::BoxFuture;
3use futures_util::FutureExt;
3use futures_util::FutureExt;
4use helix_core::auto_pairs::AutoPairs;
4use helix_core::auto_pairs::AutoPairs;
5use helix_core::Range;
5use helix_core::Range;
6+use helix_vcs::{DiffHandle, DiffProviderRegistry};
7+
6use serde::de::{self, Deserialize, Deserializer};
8use serde::de::{self, Deserialize, Deserializer};
7use serde::Serialize;
9use serde::Serialize;
8use std::borrow::Cow;
10use std::borrow::Cow;
@@ -24,6 +26,7 @@ use helix_core::{
24 DEFAULT_LINE_ENDING,
26 DEFAULT_LINE_ENDING,
25};
27};
26
28
29+use crate::editor::RedrawHandle;
27use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
30use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
28
31
29/// 8kB of buffer space for encoding and decoding `Rope`s.
32/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -133,6 +136,8 @@ pub struct Document {
133
136
134 diagnostics: Vec<Diagnostic>,
137 diagnostics: Vec<Diagnostic>,
135 language_server: Option<Arc<helix_lsp::Client>>,
138 language_server: Option<Arc<helix_lsp::Client>>,
139+
140+ diff_handle: Option<DiffHandle>,
136}
141}
137
142
138use std::{fmt, mem};
143use std::{fmt, mem};
@@ -371,6 +376,7 @@ impl Document {
371 last_saved_revision: 0,
376 last_saved_revision: 0,
372 modified_since_accessed: false,
377 modified_since_accessed: false,
373 language_server: None,
378 language_server: None,
379+ diff_handle: None,
374 }
380 }
375 }
381 }
376
382
@@ -624,16 +630,20 @@ impl Document {
624 }
630 }
625
631
626 /// Reload the document from its path.
632 /// Reload the document from its path.
627− pub fn reload(&mut self, view: &mut View) -> Result<(), Error> {
633+ pub fn reload(
634+ &mut self,
635+ view: &mut View,
636+ provider_registry: &DiffProviderRegistry,
637+ redraw_handle: RedrawHandle,
638+ ) -> Result<(), Error> {
628 let encoding = &self.encoding;
639 let encoding = &self.encoding;
640+ let path = self
641+ .path()
629− let path = self.path().filter(|path| path.exists());
642+ .filter(|path| path.exists())
630−
631− // If there is no path or the path no longer exists.
632− if path.is_none() {
633− bail!("can't find file to reload from");
643+ .ok_or_else(|| anyhow!("can't find file to reload from"))?
634− }
644+ .to_owned();
635
645
636− let mut file = std::fs::File::open(path.unwrap())?;
646+ let mut file = std::fs::File::open(&path)?;
637 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
647 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
638
648
639 // Calculate the difference between the buffer and source text, and apply it.
649 // Calculate the difference between the buffer and source text, and apply it.
@@ -646,6 +656,11 @@ impl Document {
646
656
647 self.detect_indent_and_line_ending();
657 self.detect_indent_and_line_ending();
648
658
659+ match provider_registry.get_diff_base(&path) {
660+ Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
661+ None => self.diff_handle = None,
662+ }
663+
649 Ok(())
664 Ok(())
650 }
665 }
651
666
@@ -787,6 +802,10 @@ impl Document {
787
802
788 if !transaction.changes().is_empty() {
803 if !transaction.changes().is_empty() {
789 self.version += 1;
804 self.version += 1;
805+ // start computing the diff in parallel
806+ if let Some(diff_handle) = &self.diff_handle {
807+ diff_handle.update_document(self.text.clone(), false);
808+ }
790
809
791 // generate revert to savepoint
810 // generate revert to savepoint
792 if self.savepoint.is_some() {
811 if self.savepoint.is_some() {
@@ -1046,6 +1065,23 @@ impl Document {
1046 server.is_initialized().then(|| server)
1065 server.is_initialized().then(|| server)
1047 }
1066 }
1048
1067
1068+ pub fn diff_handle(&self) -> Option<&DiffHandle> {
1069+ self.diff_handle.as_ref()
1070+ }
1071+
1072+ /// Intialize/updates the differ for this document with a new base.
1073+ pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
1074+ if let Ok((diff_base, _)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
1075+ if let Some(differ) = &self.diff_handle {
1076+ differ.update_diff_base(diff_base);
1077+ return;
1078+ }
1079+ self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
1080+ } else {
1081+ self.diff_handle = None;
1082+ }
1083+ }
1084+
1049 #[inline]
1085 #[inline]
1050 /// Tree-sitter AST tree
1086 /// Tree-sitter AST tree
1051 pub fn syntax(&self) -> Option<&Syntax> {
1087 pub fn syntax(&self) -> Option<&Syntax> {
helix-view/src/editor.rs+56−19
@@ -9,6 +9,7 @@ use crate::{
9 tree::{self, Tree},
9 tree::{self, Tree},
10 Align, Document, DocumentId, View, ViewId,
10 Align, Document, DocumentId, View, ViewId,
11};
11};
12+use helix_vcs::DiffProviderRegistry;
12
13
13use futures_util::stream::select_all::SelectAll;
14use futures_util::stream::select_all::SelectAll;
14use futures_util::{future, StreamExt};
15use futures_util::{future, StreamExt};
@@ -26,7 +27,10 @@ use std::{
26};
27};
27
28
28use tokio::{
29use tokio::{
30+ sync::{
29− sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
31+ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
32+ Notify, RwLock,
33+ },
30 time::{sleep, Duration, Instant, Sleep},
34 time::{sleep, Duration, Instant, Sleep},
31};
35};
32
36
@@ -454,6 +458,8 @@ pub enum GutterType {
454 LineNumbers,
458 LineNumbers,
455 /// Show one blank space
459 /// Show one blank space
456 Spacer,
460 Spacer,
461+ /// Highlight local changes
462+ Diff,
457}
463}
458
464
459impl std::str::FromStr for GutterType {
465impl std::str::FromStr for GutterType {
@@ -464,6 +470,7 @@ impl std::str::FromStr for GutterType {
464 "diagnostics" => Ok(Self::Diagnostics),
470 "diagnostics" => Ok(Self::Diagnostics),
465 "spacer" => Ok(Self::Spacer),
471 "spacer" => Ok(Self::Spacer),
466 "line-numbers" => Ok(Self::LineNumbers),
472 "line-numbers" => Ok(Self::LineNumbers),
473+ "diff" => Ok(Self::Diff),
467 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
474 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
468 }
475 }
469 }
476 }
@@ -600,6 +607,8 @@ impl Default for Config {
600 GutterType::Diagnostics,
607 GutterType::Diagnostics,
601 GutterType::Spacer,
608 GutterType::Spacer,
602 GutterType::LineNumbers,
609 GutterType::LineNumbers,
610+ GutterType::Spacer,
611+ GutterType::Diff,
603 ],
612 ],
604 middle_click_paste: true,
613 middle_click_paste: true,
605 auto_pairs: AutoPairConfig::default(),
614 auto_pairs: AutoPairConfig::default(),
@@ -681,6 +690,7 @@ pub struct Editor {
681 pub macro_replaying: Vec<char>,
690 pub macro_replaying: Vec<char>,
682 pub language_servers: helix_lsp::Registry,
691 pub language_servers: helix_lsp::Registry,
683 pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
692 pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
693+ pub diff_providers: DiffProviderRegistry,
684
694
685 pub debugger: Option<dap::Client>,
695 pub debugger: Option<dap::Client>,
686 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
696 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@@ -711,8 +721,15 @@ pub struct Editor {
711 pub exit_code: i32,
721 pub exit_code: i32,
712
722
713 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
723 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
724+ /// Allows asynchronous tasks to control the rendering
725+ /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
726+ /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
727+ pub redraw_handle: RedrawHandle,
728+ pub needs_redraw: bool,
714}
729}
715
730
731+pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
732+
716#[derive(Debug)]
733#[derive(Debug)]
717pub enum EditorEvent {
734pub enum EditorEvent {
718 DocumentSaved(DocumentSavedEventResult),
735 DocumentSaved(DocumentSavedEventResult),
@@ -785,6 +802,7 @@ impl Editor {
785 theme: theme_loader.default(),
802 theme: theme_loader.default(),
786 language_servers: helix_lsp::Registry::new(),
803 language_servers: helix_lsp::Registry::new(),
787 diagnostics: BTreeMap::new(),
804 diagnostics: BTreeMap::new(),
805+ diff_providers: DiffProviderRegistry::default(),
788 debugger: None,
806 debugger: None,
789 debugger_events: SelectAll::new(),
807 debugger_events: SelectAll::new(),
790 breakpoints: HashMap::new(),
808 breakpoints: HashMap::new(),
@@ -803,6 +821,8 @@ impl Editor {
803 auto_pairs,
821 auto_pairs,
804 exit_code: 0,
822 exit_code: 0,
805 config_events: unbounded_channel(),
823 config_events: unbounded_channel(),
824+ redraw_handle: Default::default(),
825+ needs_redraw: false,
806 }
826 }
807 }
827 }
808
828
@@ -1109,7 +1129,9 @@ impl Editor {
1109 let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
1129 let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
1110
1130
1111 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
1131 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
1112−
1132+ if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1133+ doc.set_diff_base(diff_base, self.redraw_handle.clone());
1134+ }
1113 self.new_document(doc)
1135 self.new_document(doc)
1114 };
1136 };
1115
1137
@@ -1348,24 +1370,39 @@ impl Editor {
1348 }
1370 }
1349
1371
1350 pub async fn wait_event(&mut self) -> EditorEvent {
1372 pub async fn wait_event(&mut self) -> EditorEvent {
1373+ // the loop only runs once or twice and would be better implemented with a recursion + const generic
1374+ // however due to limitations with async functions that can not be implemented right now
1375+ loop {
1351− tokio::select! {
1376+ tokio::select! {
1352− biased;
1377+ biased;
1378+
1379+ Some(event) = self.save_queue.next() => {
1380+ self.write_count -= 1;
1381+ return EditorEvent::DocumentSaved(event)
1382+ }
1383+ Some(config_event) = self.config_events.1.recv() => {
1384+ return EditorEvent::ConfigEvent(config_event)
1385+ }
1386+ Some(message) = self.language_servers.incoming.next() => {
1387+ return EditorEvent::LanguageServerMessage(message)
1388+ }
1389+ Some(event) = self.debugger_events.next() => {
1390+ return EditorEvent::DebuggerEvent(event)
1391+ }
1353
1392
1354− Some(event) = self.save_queue.next() => {
1393+ _ = self.redraw_handle.0.notified() => {
1355− self.write_count -= 1;
1394+ if !self.needs_redraw{
1356− EditorEvent::DocumentSaved(event)
1395+ self.needs_redraw = true;
1357− }
1396+ let timeout = Instant::now() + Duration::from_millis(96);
1358− Some(config_event) = self.config_events.1.recv() => {
1397+ if timeout < self.idle_timer.deadline(){
1359− EditorEvent::ConfigEvent(config_event)
1398+ self.idle_timer.as_mut().reset(timeout)
1360− }
1361− Some(message) = self.language_servers.incoming.next() => {
1362− EditorEvent::LanguageServerMessage(message)
1363− }
1364− Some(event) = self.debugger_events.next() => {
1365− EditorEvent::DebuggerEvent(event)
1366− }
1399+ }
1400+ }
1401+ }
1402+
1367− _ = &mut self.idle_timer => {
1403+ _ = &mut self.idle_timer => {
1368− EditorEvent::IdleTimer
1404+ return EditorEvent::IdleTimer
1405+ }
1369 }
1406 }
1370 }
1407 }
1371 }
1408 }
helix-view/src/gutter.rs+52−3
@@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize {
12 std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
12 std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
13}
13}
14
14
15−pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
15+pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, &mut String) -> Option<Style> + 'doc>;
16pub type Gutter =
16pub type Gutter =
17 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
17 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
18
18
@@ -31,6 +31,7 @@ impl GutterType {
31 }
31 }
32 GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
32 GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
33 GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
33 GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
34+ GutterType::Diff => diff(editor, doc, view, theme, is_focused),
34 }
35 }
35 }
36 }
36
37
@@ -39,6 +40,7 @@ impl GutterType {
39 GutterType::Diagnostics => 1,
40 GutterType::Diagnostics => 1,
40 GutterType::LineNumbers => line_numbers_width(_view, doc),
41 GutterType::LineNumbers => line_numbers_width(_view, doc),
41 GutterType::Spacer => 1,
42 GutterType::Spacer => 1,
43+ GutterType::Diff => 1,
42 }
44 }
43 }
45 }
44}
46}
@@ -83,6 +85,53 @@ pub fn diagnostic<'doc>(
83 })
85 })
84}
86}
85
87
88+pub fn diff<'doc>(
89+ _editor: &'doc Editor,
90+ doc: &'doc Document,
91+ _view: &View,
92+ theme: &Theme,
93+ _is_focused: bool,
94+) -> GutterFn<'doc> {
95+ let added = theme.get("diff.plus");
96+ let deleted = theme.get("diff.minus");
97+ let modified = theme.get("diff.delta");
98+ if let Some(diff_handle) = doc.diff_handle() {
99+ let hunks = diff_handle.hunks();
100+ let mut hunk_i = 0;
101+ let mut hunk = hunks.nth_hunk(hunk_i);
102+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
103+ // truncating the line is fine here because we don't compute diffs
104+ // for files with more lines than i32::MAX anyways
105+ // we need to special case removals here
106+ // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`).
107+ // However we still want to display these hunks correctly we must not yet skip to the next hunk here
108+ while hunk.after.end < line as u32
109+ || !hunk.is_pure_removal() && line as u32 == hunk.after.end
110+ {
111+ hunk_i += 1;
112+ hunk = hunks.nth_hunk(hunk_i);
113+ }
114+
115+ if hunk.after.start > line as u32 {
116+ return None;
117+ }
118+
119+ let (icon, style) = if hunk.is_pure_insertion() {
120+ ("▍", added)
121+ } else if hunk.is_pure_removal() {
122+ ("▔", deleted)
123+ } else {
124+ ("▍", modified)
125+ };
126+
127+ write!(out, "{}", icon).unwrap();
128+ Some(style)
129+ })
130+ } else {
131+ Box::new(move |_, _, _| None)
132+ }
133+}
134+
86pub fn line_numbers<'doc>(
135pub fn line_numbers<'doc>(
87 editor: &'doc Editor,
136 editor: &'doc Editor,
88 doc: &'doc Document,
137 doc: &'doc Document,
@@ -226,8 +275,8 @@ pub fn diagnostics_or_breakpoints<'doc>(
226 theme: &Theme,
275 theme: &Theme,
227 is_focused: bool,
276 is_focused: bool,
228) -> GutterFn<'doc> {
277) -> GutterFn<'doc> {
229− let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
278+ let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
230− let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
279+ let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
231
280
232 Box::new(move |line, selected, out| {
281 Box::new(move |line, selected, out| {
233 breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
282 breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
helix-view/src/view.rs+4−11
@@ -158,17 +158,10 @@ impl View {
158 }
158 }
159
159
160 pub fn gutter_offset(&self, doc: &Document) -> u16 {
160 pub fn gutter_offset(&self, doc: &Document) -> u16 {
161− let mut offset = self
162− .gutters
161+ self.gutters
163 .iter()
162 .iter()
164 .map(|gutter| gutter.width(self, doc) as u16)
163 .map(|gutter| gutter.width(self, doc) as u16)
165− .sum();
164+ .sum()
166−
167− if offset > 0 {
168− offset += 1
169− }
170−
171− offset
172 }
165 }
173
166
174 //
167 //
@@ -392,8 +385,8 @@ impl View {
392mod tests {
385mod tests {
393 use super::*;
386 use super::*;
394 use helix_core::Rope;
387 use helix_core::Rope;
395− const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
388+ const OFFSET: u16 = 3; // 1 diagnostic + 2 linenr (< 100 lines)
396− const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
389+ const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 1; // 1 diagnostic
397 // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
390 // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
398 use crate::document::Document;
391 use crate::document::Document;
399 use crate::editor::GutterType;
392 use crate::editor::GutterType;