on master help sign in
Show (git) diff signs in gutter (#3890)

* Show (git) diff signs in gutter (#3890) Avoid string allocation when git diffing Incrementally diff using changesets refactor diffs to be provider indepndent and improve git implementation remove dependency on zlib-ng switch to asynchronus diffing with similar Update helix-vcs/Cargo.toml fix toml formatting Co-authored-by: Ivan Tham <pickfire@riseup.net> fix typo in documentation use ropey reexpors from helix-core fix crash when creating new file remove useless use if io::Cursor fix spelling mistakes implement suggested improvement to repository loading improve git test isolation remove lefover comments Co-authored-by: univerz <univerz@fu-solution.com> fixed spelling mistake minor cosmetic changes fix: set self.differ to None if decoding the diff_base fails fixup formatting Co-authored-by: Ivan Tham <pickfire@riseup.net> reload diff_base when file is reloaded from disk switch to imara-diff Fixup formatting Co-authored-by: Blaž Hrastnik <blaz@mxxn.io> Redraw buffer whenever a diff is updated. Only store hunks instead of changes for individual lines to easily allow jumping between them Update to latest gitoxide version Change default diff gutter position Only update gutter after timeout * update diff gutter synchronously, with a timeout * Apply suggestions from code review Co-authored-by: Blaž Hrastnik <blaz@mxxn.io> Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * address review comments and ensure lock is always aquired * remove configuration for redraw timeout Co-authored-by: Blaž Hrastnik <blaz@mxxn.io> Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

commit5a3ff742218aac32c3af08993f0edb623631fc72 authorPascal Kuthe <pascal.kuthe@semimod.de> dateThu 1 Dec 2022, 09:35 (4 years ago) parent67415e0
21 files changed+2036−76
Cargo.lock+868−10 Cargo.toml+1 book/src/configuration.md+1−1 helix-core/src/lib.rs+1−1 helix-term/Cargo.toml+3 helix-term/src/application.rs+32−18 helix-term/src/commands/typed.rs+7−4 helix-term/src/ui/editor.rs+1−1 helix-vcs/Cargo.toml+28 helix-vcs/src/diff.rs+198 helix-vcs/src/diff/line_cache.rs+130 helix-vcs/src/diff/worker.rs+207 helix-vcs/src/diff/worker/test.rs+149 helix-vcs/src/git.rs+80 helix-vcs/src/git/test.rs+121 helix-vcs/src/lib.rs+51 helix-view/Cargo.toml+2 helix-view/src/document.rs+44−8 helix-view/src/editor.rs+56−19 helix-view/src/gutter.rs+52−3 helix-view/src/view.rs+4−11
Cargo.lock+868−10
@@ -2,6 +2,12 @@
22# It is not intended for manual editing.
33version = 3
44
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+
511[[package]]
612name = "ahash"
713version = "0.7.6"
@@ -55,6 +61,15 @@ version = "1.5.1"
5561source = "registry+https://github.com/rust-lang/crates.io-index"
5662checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
5763
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+
5873[[package]]
5974name = "autocfg"
6075version = "1.1.0"
@@ -78,12 +93,43 @@ dependencies = [
7893 "regex-automata",
7994]
8095
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+
81117[[package]]
82118name = "bumpalo"
83119version = "3.11.1"
84120source = "registry+https://github.com/rust-lang/crates.io-index"
85121checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
86122
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+
87133[[package]]
88134name = "bytecount"
89135version = "0.6.3"
@@ -96,12 +142,27 @@ version = "1.3.0"
96142source = "registry+https://github.com/rust-lang/crates.io-index"
97143checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
98144
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+
99151[[package]]
100152name = "cassowary"
101153version = "0.3.0"
102154source = "registry+https://github.com/rust-lang/crates.io-index"
103155checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
104156
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+
105166[[package]]
106167name = "cc"
107168version = "1.0.77"
@@ -148,6 +209,12 @@ dependencies = [
148209 "winapi",
149210]
150211
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+
151218[[package]]
152219name = "codespan-reporting"
153220version = "0.11.1"
@@ -158,6 +225,17 @@ dependencies = [
158225 "unicode-width",
159226]
160227
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+
161239[[package]]
162240name = "content_inspector"
163241version = "0.2.4"
@@ -173,6 +251,15 @@ version = "0.8.3"
173251source = "registry+https://github.com/rust-lang/crates.io-index"
174252checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
175253
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+
176263[[package]]
177264name = "crossbeam-utils"
178265version = "0.8.14"
@@ -252,6 +339,28 @@ dependencies = [
252339 "syn",
253340]
254341
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+
255364[[package]]
256365name = "dirs-next"
257366version = "2.0.0"
@@ -262,6 +371,17 @@ dependencies = [
262371 "dirs-sys-next",
263372]
264373
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+
265385[[package]]
266386name = "dirs-sys-next"
267387version = "0.1.2"
@@ -336,6 +456,28 @@ dependencies = [
336456 "log",
337457]
338458
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+
339481[[package]]
340482name = "fnv"
341483version = "1.0.7"
@@ -407,6 +549,498 @@ dependencies = [
407549 "wasi",
408550]
409551
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+
4101044[[package]]
4111045name = "globset"
4121046version = "0.4.9"
@@ -414,7 +1048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4141048checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
4151049dependencies = [
4161050 "aho-corasick",
417− "bstr",
1051+ "bstr 0.2.17",
4181052 "fnv",
4191053 "log",
4201054 "regex",
@@ -436,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4361070checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e"
4371071dependencies = [
4381072 "aho-corasick",
439− "bstr",
1073+ "bstr 0.2.17",
4401074 "grep-matcher",
4411075 "log",
4421076 "regex",
@@ -450,7 +1084,7 @@ version = "0.1.10"
4501084source = "registry+https://github.com/rust-lang/crates.io-index"
4511085checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc"
4521086dependencies = [
453− "bstr",
1087+ "bstr 0.2.17",
4541088 "bytecount",
4551089 "encoding_rs",
4561090 "encoding_rs_io",
@@ -459,6 +1093,12 @@ dependencies = [
4591093 "memmap2",
4601094]
4611095
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+
4621102[[package]]
4631103name = "hashbrown"
4641104version = "0.12.3"
@@ -576,6 +1216,7 @@ dependencies = [
5761216 "helix-loader",
5771217 "helix-lsp",
5781218 "helix-tui",
1219+ "helix-vcs",
5791220 "helix-view",
5801221 "ignore",
5811222 "indoc",
@@ -608,6 +1249,19 @@ dependencies = [
6081249 "unicode-segmentation",
6091250]
6101251
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+
6111265[[package]]
6121266name = "helix-view"
6131267version = "0.6.0"
@@ -624,6 +1278,7 @@ dependencies = [
6241278 "helix-loader",
6251279 "helix-lsp",
6261280 "helix-tui",
1281+ "helix-vcs",
6271282 "log",
6281283 "once_cell",
6291284 "serde",
@@ -645,6 +1300,27 @@ dependencies = [
6451300 "libc",
6461301]
6471302
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+
6481324[[package]]
6491325name = "iana-time-zone"
6501326version = "0.1.53"
@@ -722,6 +1398,16 @@ dependencies = [
7221398 "cfg-if",
7231399]
7241400
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+
7251411[[package]]
7261412name = "itoa"
7271413version = "1.0.4"
@@ -815,6 +1501,21 @@ dependencies = [
8151501 "libc",
8161502]
8171503
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+
8181519[[package]]
8191520name = "mio"
8201521version = "0.8.5"
@@ -827,6 +1528,28 @@ dependencies = [
8271528 "windows-sys",
8281529]
8291530
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+
8301553[[package]]
8311554name = "num-integer"
8321555version = "0.1.45"
@@ -856,6 +1579,15 @@ dependencies = [
8561579 "libc",
8571580]
8581581
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+
8591591[[package]]
8601592name = "once_cell"
8611593version = "1.16.0"
@@ -912,6 +1644,16 @@ dependencies = [
9121644 "unicode-ident",
9131645]
9141646
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+
9151657[[package]]
9161658name = "pulldown-cmark"
9171659version = "0.9.2"
@@ -923,6 +1665,12 @@ dependencies = [
9231665 "unicase",
9241666]
9251667
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+
9261674[[package]]
9271675name = "quickcheck"
9281676version = "1.0.3"
@@ -1021,6 +1769,12 @@ dependencies = [
10211769 "str_indices",
10221770]
10231771
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+
10241778[[package]]
10251779name = "ryu"
10261780version = "1.0.11"
@@ -1090,6 +1844,12 @@ dependencies = [
10901844 "syn",
10911845]
10921846
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+
10931853[[package]]
10941854name = "signal-hook"
10951855version = "0.3.14"
@@ -1293,6 +2053,35 @@ dependencies = [
12932053 "num_cpus",
12942054]
12952055
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+
12962085[[package]]
12972086name = "tinyvec"
12982087version = "1.6.0"
@@ -1384,6 +2173,12 @@ version = "0.3.8"
13842173source = "registry+https://github.com/rust-lang/crates.io-index"
13852174checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
13862175
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+
13872182[[package]]
13882183name = "unicode-general-category"
13892184version = "0.6.0"
@@ -1439,6 +2234,12 @@ dependencies = [
14392234 "serde",
14402235]
14412236
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+
14422243[[package]]
14432244name = "version_check"
14442245version = "0.9.4"
@@ -1558,57 +2359,114 @@ version = "0.4.0"
15582359source = "registry+https://github.com/rust-lang/crates.io-index"
15592360checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
15602361
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+
15612377[[package]]
15622378name = "windows-sys"
15632379version = "0.42.0"
15642380source = "registry+https://github.com/rust-lang/crates.io-index"
15652381checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
15662382dependencies = [
1567− "windows_aarch64_gnullvm",
1568− "windows_aarch64_msvc",
1569− "windows_i686_gnu",
1570− "windows_i686_msvc",
1571− "windows_x86_64_gnu",
1572− "windows_x86_64_gnullvm",
1573− "windows_x86_64_msvc",
2383+ "windows_aarch64_gnullvm 0.42.0",
2384+ "windows_aarch64_msvc 0.42.0",
2385+ "windows_i686_gnu 0.42.0",
2386+ "windows_i686_msvc 0.42.0",
2387+ "windows_x86_64_gnu 0.42.0",
2388+ "windows_x86_64_gnullvm 0.42.0",
2389+ "windows_x86_64_msvc 0.42.0",
15742390]
15752391
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+
15762398[[package]]
15772399name = "windows_aarch64_gnullvm"
15782400version = "0.42.0"
15792401source = "registry+https://github.com/rust-lang/crates.io-index"
15802402checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
15812403
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+
15822410[[package]]
15832411name = "windows_aarch64_msvc"
15842412version = "0.42.0"
15852413source = "registry+https://github.com/rust-lang/crates.io-index"
15862414checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
15872415
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+
15882422[[package]]
15892423name = "windows_i686_gnu"
15902424version = "0.42.0"
15912425source = "registry+https://github.com/rust-lang/crates.io-index"
15922426checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
15932427
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+
15942434[[package]]
15952435name = "windows_i686_msvc"
15962436version = "0.42.0"
15972437source = "registry+https://github.com/rust-lang/crates.io-index"
15982438checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
15992439
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+
16002446[[package]]
16012447name = "windows_x86_64_gnu"
16022448version = "0.42.0"
16032449source = "registry+https://github.com/rust-lang/crates.io-index"
16042450checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
16052451
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+
16062458[[package]]
16072459name = "windows_x86_64_gnullvm"
16082460version = "0.42.0"
16092461source = "registry+https://github.com/rust-lang/crates.io-index"
16102462checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
16112463
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+
16122470[[package]]
16132471name = "windows_x86_64_msvc"
16142472version = "0.42.0"
Cargo.toml+1
@@ -7,6 +7,7 @@ members = [
77 "helix-lsp",
88 "helix-dap",
99 "helix-loader",
10+ "helix-vcs",
1011 "xtask",
1112]
1213
book/src/configuration.md+1−1
@@ -46,7 +46,7 @@ on unix operating systems.
4646| `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` |
4747| `cursorline` | Highlight all lines with a cursor. | `false` |
4848| `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"]` |
5050| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
5151| `auto-format` | Enable automatic formatting on save. | `true` |
5252| `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
8383 top_marker.map_or(current_dir, |a| a.to_path_buf())
8484}
8585
86−pub use ropey::{str_utils, Rope, RopeBuilder, RopeSlice};
86+pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
8787
8888// pub use tendril::StrTendril as Tendril;
8989pub use smartstring::SmartString;
helix-term/Cargo.toml+3
@@ -17,8 +17,10 @@ build = true
1717app = true
1818
1919[features]
20+default = ["git"]
2021unicode-lines = ["helix-core/unicode-lines"]
2122integration = []
23+git = ["helix-vcs/git"]
2224
2325[[bin]]
2426name = "hx"
@@ -29,6 +31,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
2931helix-view = { version = "0.6", path = "../helix-view" }
3032helix-lsp = { version = "0.6", path = "../helix-lsp" }
3133helix-dap = { version = "0.6", path = "../helix-dap" }
34+helix-vcs = { version = "0.6", path = "../helix-vcs" }
3235helix-loader = { version = "0.6", path = "../helix-loader" }
3336
3437anyhow = "1"
helix-term/src/application.rs+32−18
@@ -274,16 +274,27 @@ impl Application {
274274 }
275275
276276 #[cfg(feature = "integration")]
277− fn render(&mut self) {}
277+ async fn render(&mut self) {}
278278
279279 #[cfg(not(feature = "integration"))]
280− fn render(&mut self) {
280+ async fn render(&mut self) {
281281 let mut cx = crate::compositor::Context {
282282 editor: &mut self.editor,
283283 jobs: &mut self.jobs,
284284 scroll: None,
285285 };
286286
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+
287298 let area = self
288299 .terminal
289300 .autoresize()
@@ -304,7 +315,7 @@ impl Application {
304315 where
305316 S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
306317 {
307− self.render();
318+ self.render().await;
308319 self.last_render = Instant::now();
309320
310321 loop {
@@ -329,18 +340,18 @@ impl Application {
329340 biased;
330341
331342 Some(event) = input_stream.next() => {
332− self.handle_terminal_events(event);
343+ self.handle_terminal_events(event).await;
333344 }
334345 Some(signal) = self.signals.next() => {
335346 self.handle_signals(signal).await;
336347 }
337348 Some(callback) = self.jobs.futures.next() => {
338349 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
339− self.render();
350+ self.render().await;
340351 }
341352 Some(callback) = self.jobs.wait_futures.next() => {
342353 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
343− self.render();
354+ self.render().await;
344355 }
345356 event = self.editor.wait_event() => {
346357 let _idle_handled = self.handle_editor_event(event).await;
@@ -445,25 +456,25 @@ impl Application {
445456 self.compositor.resize(area);
446457 self.terminal.clear().expect("couldn't clear terminal");
447458
448− self.render();
459+ self.render().await;
449460 }
450461 signal::SIGUSR1 => {
451462 self.refresh_config();
452− self.render();
463+ self.render().await;
453464 }
454465 _ => unreachable!(),
455466 }
456467 }
457468
458− pub fn handle_idle_timeout(&mut self) {
469+ pub async fn handle_idle_timeout(&mut self) {
459470 let mut cx = crate::compositor::Context {
460471 editor: &mut self.editor,
461472 jobs: &mut self.jobs,
462473 scroll: None,
463474 };
464475 let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
465− if should_render {
466− self.render();
476+ if should_render || self.editor.needs_redraw {
477+ self.render().await;
467478 }
468479 }
469480
@@ -536,11 +547,11 @@ impl Application {
536547 match event {
537548 EditorEvent::DocumentSaved(event) => {
538549 self.handle_document_write(event);
539− self.render();
550+ self.render().await;
540551 }
541552 EditorEvent::ConfigEvent(event) => {
542553 self.handle_config_events(event);
543− self.render();
554+ self.render().await;
544555 }
545556 EditorEvent::LanguageServerMessage((id, call)) => {
546557 self.handle_language_server_message(call, id).await;
@@ -548,19 +559,19 @@ impl Application {
548559 let last = self.editor.language_servers.incoming.is_empty();
549560
550561 if last || self.last_render.elapsed() > LSP_DEADLINE {
551− self.render();
562+ self.render().await;
552563 self.last_render = Instant::now();
553564 }
554565 }
555566 EditorEvent::DebuggerEvent(payload) => {
556567 let needs_render = self.editor.handle_debugger_message(payload).await;
557568 if needs_render {
558− self.render();
569+ self.render().await;
559570 }
560571 }
561572 EditorEvent::IdleTimer => {
562573 self.editor.clear_idle_timer();
563− self.handle_idle_timeout();
574+ self.handle_idle_timeout().await;
564575
565576 #[cfg(feature = "integration")]
566577 {
@@ -572,7 +583,10 @@ impl Application {
572583 false
573584 }
574585
575− pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) {
586+ pub async fn handle_terminal_events(
587+ &mut self,
588+ event: Result<CrosstermEvent, crossterm::ErrorKind>,
589+ ) {
576590 let mut cx = crate::compositor::Context {
577591 editor: &mut self.editor,
578592 jobs: &mut self.jobs,
@@ -596,7 +610,7 @@ impl Application {
596610 };
597611
598612 if should_redraw && !self.editor.should_close() {
599− self.render();
613+ self.render().await;
600614 }
601615 }
602616
helix-term/src/commands/typed.rs+7−4
@@ -1028,10 +1028,12 @@ fn reload(
10281028 }
10291029
10301030 let scrolloff = cx.editor.config().scrolloff;
1031+ let redraw_handle = cx.editor.redraw_handle.clone();
10311032 let (view, doc) = current!(cx.editor);
1032− doc.reload(view).map(|_| {
1033− view.ensure_cursor_in_view(doc, scrolloff);
1034− })
1033+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1034+ .map(|_| {
1035+ view.ensure_cursor_in_view(doc, scrolloff);
1036+ })
10351037}
10361038
10371039fn reload_all(
@@ -1066,7 +1068,8 @@ fn reload_all(
10661068
10671069 // Every doc is guaranteed to have at least 1 view at this point.
10681070 let view = view_mut!(cx.editor, view_ids[0]);
1069− doc.reload(view)?;
1071+ let redraw_handle = cx.editor.redraw_handle.clone();
1072+ doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
10701073
10711074 for view_id in view_ids {
10721075 let view = view_mut!(cx.editor, view_id);
helix-term/src/ui/editor.rs+1−1
@@ -730,7 +730,7 @@ impl EditorView {
730730 let mut text = String::with_capacity(8);
731731
732732 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);
734734 let width = gutter_type.width(view, doc);
735735 text.reserve(width); // ensure there's enough space for the gutter
736736 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" }
2121helix-lsp = { version = "0.6", path = "../helix-lsp" }
2222helix-dap = { version = "0.6", path = "../helix-dap" }
2323crossterm = { version = "0.25", optional = true }
24+helix-vcs = { version = "0.6", path = "../helix-vcs" }
2425
2526# Conversion traits
2627once_cell = "1.16"
@@ -43,6 +44,7 @@ log = "~0.4"
4344
4445which = "4.2"
4546
47+
4648[target.'cfg(windows)'.dependencies]
4749clipboard-win = { version = "4.4", features = ["std"] }
4850
helix-view/src/document.rs+44−8
@@ -3,6 +3,8 @@ use futures_util::future::BoxFuture;
33use futures_util::FutureExt;
44use helix_core::auto_pairs::AutoPairs;
55use helix_core::Range;
6+use helix_vcs::{DiffHandle, DiffProviderRegistry};
7+
68use serde::de::{self, Deserialize, Deserializer};
79use serde::Serialize;
810use std::borrow::Cow;
@@ -24,6 +26,7 @@ use helix_core::{
2426 DEFAULT_LINE_ENDING,
2527};
2628
29+use crate::editor::RedrawHandle;
2730use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
2831
2932/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -133,6 +136,8 @@ pub struct Document {
133136
134137 diagnostics: Vec<Diagnostic>,
135138 language_server: Option<Arc<helix_lsp::Client>>,
139+
140+ diff_handle: Option<DiffHandle>,
136141}
137142
138143use std::{fmt, mem};
@@ -371,6 +376,7 @@ impl Document {
371376 last_saved_revision: 0,
372377 modified_since_accessed: false,
373378 language_server: None,
379+ diff_handle: None,
374380 }
375381 }
376382
@@ -624,16 +630,20 @@ impl Document {
624630 }
625631
626632 /// 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> {
628639 let encoding = &self.encoding;
629− let path = self.path().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");
634− }
640+ let path = self
641+ .path()
642+ .filter(|path| path.exists())
643+ .ok_or_else(|| anyhow!("can't find file to reload from"))?
644+ .to_owned();
635645
636− let mut file = std::fs::File::open(path.unwrap())?;
646+ let mut file = std::fs::File::open(&path)?;
637647 let (rope, ..) = from_reader(&mut file, Some(encoding))?;
638648
639649 // Calculate the difference between the buffer and source text, and apply it.
@@ -646,6 +656,11 @@ impl Document {
646656
647657 self.detect_indent_and_line_ending();
648658
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+
649664 Ok(())
650665 }
651666
@@ -787,6 +802,10 @@ impl Document {
787802
788803 if !transaction.changes().is_empty() {
789804 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+ }
790809
791810 // generate revert to savepoint
792811 if self.savepoint.is_some() {
@@ -1046,6 +1065,23 @@ impl Document {
10461065 server.is_initialized().then(|| server)
10471066 }
10481067
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+
10491085 #[inline]
10501086 /// Tree-sitter AST tree
10511087 pub fn syntax(&self) -> Option<&Syntax> {
helix-view/src/editor.rs+56−19
@@ -9,6 +9,7 @@ use crate::{
99 tree::{self, Tree},
1010 Align, Document, DocumentId, View, ViewId,
1111};
12+use helix_vcs::DiffProviderRegistry;
1213
1314use futures_util::stream::select_all::SelectAll;
1415use futures_util::{future, StreamExt};
@@ -26,7 +27,10 @@ use std::{
2627};
2728
2829use tokio::{
29− sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
30+ sync::{
31+ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
32+ Notify, RwLock,
33+ },
3034 time::{sleep, Duration, Instant, Sleep},
3135};
3236
@@ -454,6 +458,8 @@ pub enum GutterType {
454458 LineNumbers,
455459 /// Show one blank space
456460 Spacer,
461+ /// Highlight local changes
462+ Diff,
457463}
458464
459465impl std::str::FromStr for GutterType {
@@ -464,6 +470,7 @@ impl std::str::FromStr for GutterType {
464470 "diagnostics" => Ok(Self::Diagnostics),
465471 "spacer" => Ok(Self::Spacer),
466472 "line-numbers" => Ok(Self::LineNumbers),
473+ "diff" => Ok(Self::Diff),
467474 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
468475 }
469476 }
@@ -600,6 +607,8 @@ impl Default for Config {
600607 GutterType::Diagnostics,
601608 GutterType::Spacer,
602609 GutterType::LineNumbers,
610+ GutterType::Spacer,
611+ GutterType::Diff,
603612 ],
604613 middle_click_paste: true,
605614 auto_pairs: AutoPairConfig::default(),
@@ -681,6 +690,7 @@ pub struct Editor {
681690 pub macro_replaying: Vec<char>,
682691 pub language_servers: helix_lsp::Registry,
683692 pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
693+ pub diff_providers: DiffProviderRegistry,
684694
685695 pub debugger: Option<dap::Client>,
686696 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@@ -711,8 +721,15 @@ pub struct Editor {
711721 pub exit_code: i32,
712722
713723 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,
714729}
715730
731+pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
732+
716733#[derive(Debug)]
717734pub enum EditorEvent {
718735 DocumentSaved(DocumentSavedEventResult),
@@ -785,6 +802,7 @@ impl Editor {
785802 theme: theme_loader.default(),
786803 language_servers: helix_lsp::Registry::new(),
787804 diagnostics: BTreeMap::new(),
805+ diff_providers: DiffProviderRegistry::default(),
788806 debugger: None,
789807 debugger_events: SelectAll::new(),
790808 breakpoints: HashMap::new(),
@@ -803,6 +821,8 @@ impl Editor {
803821 auto_pairs,
804822 exit_code: 0,
805823 config_events: unbounded_channel(),
824+ redraw_handle: Default::default(),
825+ needs_redraw: false,
806826 }
807827 }
808828
@@ -1109,7 +1129,9 @@ impl Editor {
11091129 let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
11101130
11111131 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+ }
11131135 self.new_document(doc)
11141136 };
11151137
@@ -1348,24 +1370,39 @@ impl Editor {
13481370 }
13491371
13501372 pub async fn wait_event(&mut self) -> EditorEvent {
1351− tokio::select! {
1352− biased;
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 {
1376+ tokio::select! {
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+ }
13531392
1354− Some(event) = self.save_queue.next() => {
1355− self.write_count -= 1;
1356− EditorEvent::DocumentSaved(event)
1357− }
1358− Some(config_event) = self.config_events.1.recv() => {
1359− EditorEvent::ConfigEvent(config_event)
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− }
1367− _ = &mut self.idle_timer => {
1368− EditorEvent::IdleTimer
1393+ _ = self.redraw_handle.0.notified() => {
1394+ if !self.needs_redraw{
1395+ self.needs_redraw = true;
1396+ let timeout = Instant::now() + Duration::from_millis(96);
1397+ if timeout < self.idle_timer.deadline(){
1398+ self.idle_timer.as_mut().reset(timeout)
1399+ }
1400+ }
1401+ }
1402+
1403+ _ = &mut self.idle_timer => {
1404+ return EditorEvent::IdleTimer
1405+ }
13691406 }
13701407 }
13711408 }
helix-view/src/gutter.rs+52−3
@@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize {
1212 std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
1313}
1414
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>;
1616pub type Gutter =
1717 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
1818
@@ -31,6 +31,7 @@ impl GutterType {
3131 }
3232 GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
3333 GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
34+ GutterType::Diff => diff(editor, doc, view, theme, is_focused),
3435 }
3536 }
3637
@@ -39,6 +40,7 @@ impl GutterType {
3940 GutterType::Diagnostics => 1,
4041 GutterType::LineNumbers => line_numbers_width(_view, doc),
4142 GutterType::Spacer => 1,
43+ GutterType::Diff => 1,
4244 }
4345 }
4446}
@@ -83,6 +85,53 @@ pub fn diagnostic<'doc>(
8385 })
8486}
8587
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+
86135pub fn line_numbers<'doc>(
87136 editor: &'doc Editor,
88137 doc: &'doc Document,
@@ -226,8 +275,8 @@ pub fn diagnostics_or_breakpoints<'doc>(
226275 theme: &Theme,
227276 is_focused: bool,
228277) -> GutterFn<'doc> {
229− let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
230− let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
278+ let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
279+ let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
231280
232281 Box::new(move |line, selected, out| {
233282 breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
helix-view/src/view.rs+4−11
@@ -158,17 +158,10 @@ impl View {
158158 }
159159
160160 pub fn gutter_offset(&self, doc: &Document) -> u16 {
161− let mut offset = self
162− .gutters
161+ self.gutters
163162 .iter()
164163 .map(|gutter| gutter.width(self, doc) as u16)
165− .sum();
166−
167− if offset > 0 {
168− offset += 1
169− }
170−
171− offset
164+ .sum()
172165 }
173166
174167 //
@@ -392,8 +385,8 @@ impl View {
392385mod tests {
393386 use super::*;
394387 use helix_core::Rope;
395− const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
396− const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
388+ const OFFSET: u16 = 3; // 1 diagnostic + 2 linenr (< 100 lines)
389+ const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 1; // 1 diagnostic
397390 // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
398391 use crate::document::Document;
399392 use crate::editor::GutterType;