on master help sign in
Merge pull request #2359 from dead10ck/test-harness
19dccadBlaž Hrastnik
.cargo/config+1
@@ -1,2 +1,3 @@
1[alias]
1[alias]
2xtask = "run --package xtask --"
2xtask = "run --package xtask --"
3+integration-test = "test --features integration --workspace --test integration"
.github/workflows/build.yml+8−3
@@ -38,6 +38,9 @@ jobs:
38 test:
38 test:
39 name: Test Suite
39 name: Test Suite
40 runs-on: ${{ matrix.os }}
40 runs-on: ${{ matrix.os }}
41+ env:
42+ RUST_BACKTRACE: 1
43+ HELIX_LOG_LEVEL: info
41 steps:
44 steps:
42 - name: Checkout sources
45 - name: Checkout sources
43 uses: actions/checkout@v3
46 uses: actions/checkout@v3
@@ -50,9 +53,6 @@ jobs:
50
53
51 - uses: Swatinem/rust-cache@v1
54 - uses: Swatinem/rust-cache@v1
52
55
53− - name: Copy minimal languages config
54− run: cp .github/workflows/languages.toml ./languages.toml
55−
56 - name: Cache test tree-sitter grammar
56 - name: Cache test tree-sitter grammar
57 uses: actions/cache@v3
57 uses: actions/cache@v3
58 with:
58 with:
@@ -66,6 +66,11 @@ jobs:
66 command: test
66 command: test
67 args: --workspace
67 args: --workspace
68
68
69+ - name: Run cargo integration-test
70+ uses: actions-rs/cargo@v1
71+ with:
72+ command: integration-test
73+
69 strategy:
74 strategy:
70 matrix:
75 matrix:
71 os: [ubuntu-latest, macos-latest, windows-latest]
76 os: [ubuntu-latest, macos-latest, windows-latest]
.gitignore−1
@@ -1,6 +1,5 @@
1target
1target
2.direnv
2.direnv
3helix-term/rustfmt.toml
3helix-term/rustfmt.toml
4−helix-syntax/languages/
5result
4result
6runtime/grammars
5runtime/grammars
Cargo.lock+227−96
@@ -25,9 +25,9 @@ checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f"
25
25
26[[package]]
26[[package]]
27name = "autocfg"
27name = "autocfg"
28−version = "1.1.0"
28+version = "1.0.1"
29source = "registry+https://github.com/rust-lang/crates.io-index"
29source = "registry+https://github.com/rust-lang/crates.io-index"
30−checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
30+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
31
31
32[[package]]
32[[package]]
33name = "bitflags"
33name = "bitflags"
@@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
66
66
67[[package]]
67[[package]]
68name = "cc"
68name = "cc"
69−version = "1.0.73"
69+version = "1.0.72"
70source = "registry+https://github.com/rust-lang/crates.io-index"
70source = "registry+https://github.com/rust-lang/crates.io-index"
71−checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
71+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
72
72
73[[package]]
73[[package]]
74name = "cfg-if"
74name = "cfg-if"
@@ -121,9 +121,9 @@ dependencies = [
121
121
122[[package]]
122[[package]]
123name = "crossbeam-utils"
123name = "crossbeam-utils"
124−version = "0.8.8"
124+version = "0.8.7"
125source = "registry+https://github.com/rust-lang/crates.io-index"
125source = "registry+https://github.com/rust-lang/crates.io-index"
126−checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
126+checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
127dependencies = [
127dependencies = [
128 "cfg-if",
128 "cfg-if",
129 "lazy_static",
129 "lazy_static",
@@ -131,15 +131,15 @@ dependencies = [
131
131
132[[package]]
132[[package]]
133name = "crossterm"
133name = "crossterm"
134−version = "0.23.2"
134+version = "0.23.0"
135source = "registry+https://github.com/rust-lang/crates.io-index"
135source = "registry+https://github.com/rust-lang/crates.io-index"
136−checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
136+checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432"
137dependencies = [
137dependencies = [
138 "bitflags",
138 "bitflags",
139 "crossterm_winapi",
139 "crossterm_winapi",
140 "futures-core",
140 "futures-core",
141 "libc",
141 "libc",
142− "mio",
142+ "mio 0.7.14",
143 "parking_lot",
143 "parking_lot",
144 "signal-hook",
144 "signal-hook",
145 "signal-hook-mio",
145 "signal-hook-mio",
@@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
184
184
185[[package]]
185[[package]]
186name = "encoding_rs"
186name = "encoding_rs"
187−version = "0.8.31"
187+version = "0.8.30"
188source = "registry+https://github.com/rust-lang/crates.io-index"
188source = "registry+https://github.com/rust-lang/crates.io-index"
189−checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
189+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
190dependencies = [
190dependencies = [
191 "cfg-if",
191 "cfg-if",
192]
192]
@@ -221,6 +221,15 @@ dependencies = [
221 "thiserror",
221 "thiserror",
222]
222]
223
223
224+[[package]]
225+name = "fastrand"
226+version = "1.7.0"
227+source = "registry+https://github.com/rust-lang/crates.io-index"
228+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
229+dependencies = [
230+ "instant",
231+]
232+
224[[package]]
233[[package]]
225name = "fern"
234name = "fern"
226version = "0.6.1"
235version = "0.6.1"
@@ -293,9 +302,9 @@ dependencies = [
293
302
294[[package]]
303[[package]]
295name = "getrandom"
304name = "getrandom"
296−version = "0.2.6"
305+version = "0.2.4"
297source = "registry+https://github.com/rust-lang/crates.io-index"
306source = "registry+https://github.com/rust-lang/crates.io-index"
298−checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
307+checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
299dependencies = [
308dependencies = [
300 "cfg-if",
309 "cfg-if",
301 "libc",
310 "libc",
@@ -452,6 +461,7 @@ dependencies = [
452 "helix-tui",
461 "helix-tui",
453 "helix-view",
462 "helix-view",
454 "ignore",
463 "ignore",
464+ "indoc",
455 "log",
465 "log",
456 "once_cell",
466 "once_cell",
457 "pulldown-cmark",
467 "pulldown-cmark",
@@ -460,6 +470,8 @@ dependencies = [
460 "serde_json",
470 "serde_json",
461 "signal-hook",
471 "signal-hook",
462 "signal-hook-tokio",
472 "signal-hook-tokio",
473+ "smallvec",
474+ "tempfile",
463 "tokio",
475 "tokio",
464 "tokio-stream",
476 "tokio-stream",
465 "toml",
477 "toml",
@@ -544,11 +556,29 @@ dependencies = [
544 "winapi-util",
556 "winapi-util",
545]
557]
546
558
559+[[package]]
560+name = "indoc"
561+version = "1.0.3"
562+source = "registry+https://github.com/rust-lang/crates.io-index"
563+checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136"
564+dependencies = [
565+ "unindent",
566+]
567+
568+[[package]]
569+name = "instant"
570+version = "0.1.12"
571+source = "registry+https://github.com/rust-lang/crates.io-index"
572+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
573+dependencies = [
574+ "cfg-if",
575+]
576+
547[[package]]
577[[package]]
548name = "itoa"
578name = "itoa"
549−version = "1.0.2"
579+version = "1.0.1"
550source = "registry+https://github.com/rust-lang/crates.io-index"
580source = "registry+https://github.com/rust-lang/crates.io-index"
551−checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
581+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
552
582
553[[package]]
583[[package]]
554name = "lazy_static"
584name = "lazy_static"
@@ -558,9 +588,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
558
588
559[[package]]
589[[package]]
560name = "libc"
590name = "libc"
561−version = "0.2.126"
591+version = "0.2.125"
562source = "registry+https://github.com/rust-lang/crates.io-index"
592source = "registry+https://github.com/rust-lang/crates.io-index"
563−checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
593+checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
564
594
565[[package]]
595[[package]]
566name = "libloading"
596name = "libloading"
@@ -574,19 +604,18 @@ dependencies = [
574
604
575[[package]]
605[[package]]
576name = "lock_api"
606name = "lock_api"
577−version = "0.4.7"
607+version = "0.4.6"
578source = "registry+https://github.com/rust-lang/crates.io-index"
608source = "registry+https://github.com/rust-lang/crates.io-index"
579−checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
609+checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
580dependencies = [
610dependencies = [
581− "autocfg",
582 "scopeguard",
611 "scopeguard",
583]
612]
584
613
585[[package]]
614[[package]]
586name = "log"
615name = "log"
587−version = "0.4.17"
616+version = "0.4.14"
588source = "registry+https://github.com/rust-lang/crates.io-index"
617source = "registry+https://github.com/rust-lang/crates.io-index"
589−checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
618+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
590dependencies = [
619dependencies = [
591 "cfg-if",
620 "cfg-if",
592]
621]
@@ -612,9 +641,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
612
641
613[[package]]
642[[package]]
614name = "memchr"
643name = "memchr"
615−version = "2.5.0"
644+version = "2.4.1"
616source = "registry+https://github.com/rust-lang/crates.io-index"
645source = "registry+https://github.com/rust-lang/crates.io-index"
617−checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
646+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
618
647
619[[package]]
648[[package]]
620name = "memmap2"
649name = "memmap2"
@@ -625,6 +654,19 @@ dependencies = [
625 "libc",
654 "libc",
626]
655]
627
656
657+[[package]]
658+name = "mio"
659+version = "0.7.14"
660+source = "registry+https://github.com/rust-lang/crates.io-index"
661+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
662+dependencies = [
663+ "libc",
664+ "log",
665+ "miow",
666+ "ntapi",
667+ "winapi",
668+]
669+
628[[package]]
670[[package]]
629name = "mio"
671name = "mio"
630version = "0.8.3"
672version = "0.8.3"
@@ -634,14 +676,32 @@ dependencies = [
634 "libc",
676 "libc",
635 "log",
677 "log",
636 "wasi 0.11.0+wasi-snapshot-preview1",
678 "wasi 0.11.0+wasi-snapshot-preview1",
637− "windows-sys",
679+ "windows-sys 0.36.1",
680+]
681+
682+[[package]]
683+name = "miow"
684+version = "0.3.7"
685+source = "registry+https://github.com/rust-lang/crates.io-index"
686+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
687+dependencies = [
688+ "winapi",
689+]
690+
691+[[package]]
692+name = "ntapi"
693+version = "0.3.6"
694+source = "registry+https://github.com/rust-lang/crates.io-index"
695+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
696+dependencies = [
697+ "winapi",
638]
698]
639
699
640[[package]]
700[[package]]
641name = "num-integer"
701name = "num-integer"
642−version = "0.1.45"
702+version = "0.1.44"
643source = "registry+https://github.com/rust-lang/crates.io-index"
703source = "registry+https://github.com/rust-lang/crates.io-index"
644−checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
704+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
645dependencies = [
705dependencies = [
646 "autocfg",
706 "autocfg",
647 "num-traits",
707 "num-traits",
@@ -649,9 +709,9 @@ dependencies = [
649
709
650[[package]]
710[[package]]
651name = "num-traits"
711name = "num-traits"
652−version = "0.2.15"
712+version = "0.2.14"
653source = "registry+https://github.com/rust-lang/crates.io-index"
713source = "registry+https://github.com/rust-lang/crates.io-index"
654−checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
714+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
655dependencies = [
715dependencies = [
656 "autocfg",
716 "autocfg",
657]
717]
@@ -684,15 +744,15 @@ dependencies = [
684
744
685[[package]]
745[[package]]
686name = "parking_lot_core"
746name = "parking_lot_core"
687−version = "0.9.3"
747+version = "0.9.1"
688source = "registry+https://github.com/rust-lang/crates.io-index"
748source = "registry+https://github.com/rust-lang/crates.io-index"
689−checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
749+checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954"
690dependencies = [
750dependencies = [
691 "cfg-if",
751 "cfg-if",
692 "libc",
752 "libc",
693 "redox_syscall",
753 "redox_syscall",
694 "smallvec",
754 "smallvec",
695− "windows-sys",
755+ "windows-sys 0.32.0",
696]
756]
697
757
698[[package]]
758[[package]]
@@ -703,9 +763,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
703
763
704[[package]]
764[[package]]
705name = "pin-project-lite"
765name = "pin-project-lite"
706−version = "0.2.9"
766+version = "0.2.8"
707source = "registry+https://github.com/rust-lang/crates.io-index"
767source = "registry+https://github.com/rust-lang/crates.io-index"
708−checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
768+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
709
769
710[[package]]
770[[package]]
711name = "pin-utils"
771name = "pin-utils"
@@ -715,11 +775,11 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
715
775
716[[package]]
776[[package]]
717name = "proc-macro2"
777name = "proc-macro2"
718−version = "1.0.39"
778+version = "1.0.36"
719source = "registry+https://github.com/rust-lang/crates.io-index"
779source = "registry+https://github.com/rust-lang/crates.io-index"
720−checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
780+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
721dependencies = [
781dependencies = [
722− "unicode-ident",
782+ "unicode-xid",
723]
783]
724
784
725[[package]]
785[[package]]
@@ -744,18 +804,18 @@ dependencies = [
744
804
745[[package]]
805[[package]]
746name = "quote"
806name = "quote"
747−version = "1.0.18"
807+version = "1.0.15"
748source = "registry+https://github.com/rust-lang/crates.io-index"
808source = "registry+https://github.com/rust-lang/crates.io-index"
749−checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
809+checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
750dependencies = [
810dependencies = [
751 "proc-macro2",
811 "proc-macro2",
752]
812]
753
813
754[[package]]
814[[package]]
755name = "rand"
815name = "rand"
756−version = "0.8.5"
816+version = "0.8.4"
757source = "registry+https://github.com/rust-lang/crates.io-index"
817source = "registry+https://github.com/rust-lang/crates.io-index"
758−checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
818+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
759dependencies = [
819dependencies = [
760 "rand_core",
820 "rand_core",
761]
821]
@@ -771,29 +831,28 @@ dependencies = [
771
831
772[[package]]
832[[package]]
773name = "redox_syscall"
833name = "redox_syscall"
774−version = "0.2.13"
834+version = "0.2.10"
775source = "registry+https://github.com/rust-lang/crates.io-index"
835source = "registry+https://github.com/rust-lang/crates.io-index"
776−checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
836+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
777dependencies = [
837dependencies = [
778 "bitflags",
838 "bitflags",
779]
839]
780
840
781[[package]]
841[[package]]
782name = "redox_users"
842name = "redox_users"
783−version = "0.4.3"
843+version = "0.4.0"
784source = "registry+https://github.com/rust-lang/crates.io-index"
844source = "registry+https://github.com/rust-lang/crates.io-index"
785−checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
845+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
786dependencies = [
846dependencies = [
787 "getrandom",
847 "getrandom",
788 "redox_syscall",
848 "redox_syscall",
789− "thiserror",
790]
849]
791
850
792[[package]]
851[[package]]
793name = "regex"
852name = "regex"
794−version = "1.5.6"
853+version = "1.5.4"
795source = "registry+https://github.com/rust-lang/crates.io-index"
854source = "registry+https://github.com/rust-lang/crates.io-index"
796−checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
855+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
797dependencies = [
856dependencies = [
798 "aho-corasick",
857 "aho-corasick",
799 "memchr",
858 "memchr",
@@ -808,15 +867,24 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
808
867
809[[package]]
868[[package]]
810name = "regex-syntax"
869name = "regex-syntax"
811−version = "0.6.26"
870+version = "0.6.25"
871+source = "registry+https://github.com/rust-lang/crates.io-index"
872+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
873+
874+[[package]]
875+name = "remove_dir_all"
876+version = "0.5.3"
812source = "registry+https://github.com/rust-lang/crates.io-index"
877source = "registry+https://github.com/rust-lang/crates.io-index"
813−checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
878+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
879+dependencies = [
880+ "winapi",
881+]
814
882
815[[package]]
883[[package]]
816name = "retain_mut"
884name = "retain_mut"
817−version = "0.1.9"
885+version = "0.1.7"
818source = "registry+https://github.com/rust-lang/crates.io-index"
886source = "registry+https://github.com/rust-lang/crates.io-index"
819−checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0"
887+checksum = "8c31b5c4033f8fdde8700e4657be2c497e7288f01515be52168c631e2e4d4086"
820
888
821[[package]]
889[[package]]
822name = "ropey"
890name = "ropey"
@@ -830,9 +898,9 @@ dependencies = [
830
898
831[[package]]
899[[package]]
832name = "ryu"
900name = "ryu"
833−version = "1.0.10"
901+version = "1.0.9"
834source = "registry+https://github.com/rust-lang/crates.io-index"
902source = "registry+https://github.com/rust-lang/crates.io-index"
835−checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
903+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
836
904
837[[package]]
905[[package]]
838name = "same-file"
906name = "same-file"
@@ -851,18 +919,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
851
919
852[[package]]
920[[package]]
853name = "serde"
921name = "serde"
854−version = "1.0.137"
922+version = "1.0.136"
855source = "registry+https://github.com/rust-lang/crates.io-index"
923source = "registry+https://github.com/rust-lang/crates.io-index"
856−checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
924+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
857dependencies = [
925dependencies = [
858 "serde_derive",
926 "serde_derive",
859]
927]
860
928
861[[package]]
929[[package]]
862name = "serde_derive"
930name = "serde_derive"
863−version = "1.0.137"
931+version = "1.0.136"
864source = "registry+https://github.com/rust-lang/crates.io-index"
932source = "registry+https://github.com/rust-lang/crates.io-index"
865−checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
933+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
866dependencies = [
934dependencies = [
867 "proc-macro2",
935 "proc-macro2",
868 "quote",
936 "quote",
@@ -871,9 +939,9 @@ dependencies = [
871
939
872[[package]]
940[[package]]
873name = "serde_json"
941name = "serde_json"
874−version = "1.0.81"
942+version = "1.0.79"
875source = "registry+https://github.com/rust-lang/crates.io-index"
943source = "registry+https://github.com/rust-lang/crates.io-index"
876−checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
944+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
877dependencies = [
945dependencies = [
878 "itoa",
946 "itoa",
879 "ryu",
947 "ryu",
@@ -882,9 +950,9 @@ dependencies = [
882
950
883[[package]]
951[[package]]
884name = "serde_repr"
952name = "serde_repr"
885−version = "0.1.8"
953+version = "0.1.7"
886source = "registry+https://github.com/rust-lang/crates.io-index"
954source = "registry+https://github.com/rust-lang/crates.io-index"
887−checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed"
955+checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5"
888dependencies = [
956dependencies = [
889 "proc-macro2",
957 "proc-macro2",
890 "quote",
958 "quote",
@@ -903,12 +971,12 @@ dependencies = [
903
971
904[[package]]
972[[package]]
905name = "signal-hook-mio"
973name = "signal-hook-mio"
906−version = "0.2.3"
974+version = "0.2.1"
907source = "registry+https://github.com/rust-lang/crates.io-index"
975source = "registry+https://github.com/rust-lang/crates.io-index"
908−checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
976+checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
909dependencies = [
977dependencies = [
910 "libc",
978 "libc",
911− "mio",
979+ "mio 0.7.14",
912 "signal-hook",
980 "signal-hook",
913]
981]
914
982
@@ -941,9 +1009,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
941
1009
942[[package]]
1010[[package]]
943name = "slab"
1011name = "slab"
944−version = "0.4.6"
1012+version = "0.4.5"
945source = "registry+https://github.com/rust-lang/crates.io-index"
1013source = "registry+https://github.com/rust-lang/crates.io-index"
946−checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
1014+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
947
1015
948[[package]]
1016[[package]]
949name = "slotmap"
1017name = "slotmap"
@@ -995,9 +1063,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
995
1063
996[[package]]
1064[[package]]
997name = "str-buf"
1065name = "str-buf"
998−version = "1.0.6"
1066+version = "1.0.5"
999source = "registry+https://github.com/rust-lang/crates.io-index"
1067source = "registry+https://github.com/rust-lang/crates.io-index"
1000−checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
1068+checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
1001
1069
1002[[package]]
1070[[package]]
1003name = "str_indices"
1071name = "str_indices"
@@ -1007,13 +1075,27 @@ checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0"
1007
1075
1008[[package]]
1076[[package]]
1009name = "syn"
1077name = "syn"
1010−version = "1.0.95"
1078+version = "1.0.86"
1011source = "registry+https://github.com/rust-lang/crates.io-index"
1079source = "registry+https://github.com/rust-lang/crates.io-index"
1012−checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
1080+checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
1013dependencies = [
1081dependencies = [
1014 "proc-macro2",
1082 "proc-macro2",
1015 "quote",
1083 "quote",
1016− "unicode-ident",
1084+ "unicode-xid",
1085+]
1086+
1087+[[package]]
1088+name = "tempfile"
1089+version = "3.3.0"
1090+source = "registry+https://github.com/rust-lang/crates.io-index"
1091+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
1092+dependencies = [
1093+ "cfg-if",
1094+ "fastrand",
1095+ "libc",
1096+ "redox_syscall",
1097+ "remove_dir_all",
1098+ "winapi",
1017]
1099]
1018
1100
1019[[package]]
1101[[package]]
@@ -1029,18 +1111,18 @@ dependencies = [
1029
1111
1030[[package]]
1112[[package]]
1031name = "thiserror"
1113name = "thiserror"
1032−version = "1.0.31"
1114+version = "1.0.30"
1033source = "registry+https://github.com/rust-lang/crates.io-index"
1115source = "registry+https://github.com/rust-lang/crates.io-index"
1034−checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
1116+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
1035dependencies = [
1117dependencies = [
1036 "thiserror-impl",
1118 "thiserror-impl",
1037]
1119]
1038
1120
1039[[package]]
1121[[package]]
1040name = "thiserror-impl"
1122name = "thiserror-impl"
1041−version = "1.0.31"
1123+version = "1.0.30"
1042source = "registry+https://github.com/rust-lang/crates.io-index"
1124source = "registry+https://github.com/rust-lang/crates.io-index"
1043−checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
1125+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
1044dependencies = [
1126dependencies = [
1045 "proc-macro2",
1127 "proc-macro2",
1046 "quote",
1128 "quote",
@@ -1067,9 +1149,9 @@ dependencies = [
1067
1149
1068[[package]]
1150[[package]]
1069name = "tinyvec"
1151name = "tinyvec"
1070−version = "1.6.0"
1152+version = "1.5.1"
1071source = "registry+https://github.com/rust-lang/crates.io-index"
1153source = "registry+https://github.com/rust-lang/crates.io-index"
1072−checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
1154+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
1073dependencies = [
1155dependencies = [
1074 "tinyvec_macros",
1156 "tinyvec_macros",
1075]
1157]
@@ -1089,7 +1171,7 @@ dependencies = [
1089 "bytes",
1171 "bytes",
1090 "libc",
1172 "libc",
1091 "memchr",
1173 "memchr",
1092− "mio",
1174+ "mio 0.8.3",
1093 "num_cpus",
1175 "num_cpus",
1094 "once_cell",
1176 "once_cell",
1095 "parking_lot",
1177 "parking_lot",
@@ -1133,9 +1215,9 @@ dependencies = [
1133
1215
1134[[package]]
1216[[package]]
1135name = "tree-sitter"
1217name = "tree-sitter"
1136−version = "0.20.6"
1218+version = "0.20.4"
1137source = "registry+https://github.com/rust-lang/crates.io-index"
1219source = "registry+https://github.com/rust-lang/crates.io-index"
1138−checksum = "09b3b781640108d29892e8b9684642d2cda5ea05951fd58f0fea1db9edeb9b71"
1220+checksum = "4e34327f8eac545e3f037382471b2b19367725a242bba7bc45edb9efb49fe39a"
1139dependencies = [
1221dependencies = [
1140 "cc",
1222 "cc",
1141 "regex",
1223 "regex",
@@ -1152,9 +1234,9 @@ dependencies = [
1152
1234
1153[[package]]
1235[[package]]
1154name = "unicode-bidi"
1236name = "unicode-bidi"
1155−version = "0.3.8"
1237+version = "0.3.7"
1156source = "registry+https://github.com/rust-lang/crates.io-index"
1238source = "registry+https://github.com/rust-lang/crates.io-index"
1157−checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
1239+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
1158
1240
1159[[package]]
1241[[package]]
1160name = "unicode-general-category"
1242name = "unicode-general-category"
@@ -1162,12 +1244,6 @@ version = "0.5.1"
1162source = "registry+https://github.com/rust-lang/crates.io-index"
1244source = "registry+https://github.com/rust-lang/crates.io-index"
1163checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6"
1245checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6"
1164
1246
1165−[[package]]
1166−name = "unicode-ident"
1167−version = "1.0.0"
1168−source = "registry+https://github.com/rust-lang/crates.io-index"
1169−checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
1170−
1171[[package]]
1247[[package]]
1172name = "unicode-linebreak"
1248name = "unicode-linebreak"
1173version = "0.1.2"
1249version = "0.1.2"
@@ -1198,6 +1274,18 @@ version = "0.1.9"
1198source = "registry+https://github.com/rust-lang/crates.io-index"
1274source = "registry+https://github.com/rust-lang/crates.io-index"
1199checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
1275checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
1200
1276
1277+[[package]]
1278+name = "unicode-xid"
1279+version = "0.2.2"
1280+source = "registry+https://github.com/rust-lang/crates.io-index"
1281+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
1282+
1283+[[package]]
1284+name = "unindent"
1285+version = "0.1.7"
1286+source = "registry+https://github.com/rust-lang/crates.io-index"
1287+checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
1288+
1201[[package]]
1289[[package]]
1202name = "url"
1290name = "url"
1203version = "2.2.2"
1291version = "2.2.2"
@@ -1242,9 +1330,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1242
1330
1243[[package]]
1331[[package]]
1244name = "which"
1332name = "which"
1245−version = "4.2.5"
1333+version = "4.2.4"
1246source = "registry+https://github.com/rust-lang/crates.io-index"
1334source = "registry+https://github.com/rust-lang/crates.io-index"
1247−checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae"
1335+checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2"
1248dependencies = [
1336dependencies = [
1249 "either",
1337 "either",
1250 "lazy_static",
1338 "lazy_static",
@@ -1282,43 +1370,86 @@ version = "0.4.0"
1282source = "registry+https://github.com/rust-lang/crates.io-index"
1370source = "registry+https://github.com/rust-lang/crates.io-index"
1283checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1371checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1284
1372
1373+[[package]]
1374+name = "windows-sys"
1375+version = "0.32.0"
1376+source = "registry+https://github.com/rust-lang/crates.io-index"
1377+checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6"
1378+dependencies = [
1379+ "windows_aarch64_msvc 0.32.0",
1380+ "windows_i686_gnu 0.32.0",
1381+ "windows_i686_msvc 0.32.0",
1382+ "windows_x86_64_gnu 0.32.0",
1383+ "windows_x86_64_msvc 0.32.0",
1384+]
1385+
1285[[package]]
1386[[package]]
1286name = "windows-sys"
1387name = "windows-sys"
1287version = "0.36.1"
1388version = "0.36.1"
1288source = "registry+https://github.com/rust-lang/crates.io-index"
1389source = "registry+https://github.com/rust-lang/crates.io-index"
1289checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
1390checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
1290dependencies = [
1391dependencies = [
1291− "windows_aarch64_msvc",
1392+ "windows_aarch64_msvc 0.36.1",
1292− "windows_i686_gnu",
1393+ "windows_i686_gnu 0.36.1",
1293− "windows_i686_msvc",
1394+ "windows_i686_msvc 0.36.1",
1294− "windows_x86_64_gnu",
1395+ "windows_x86_64_gnu 0.36.1",
1295− "windows_x86_64_msvc",
1396+ "windows_x86_64_msvc 0.36.1",
1296]
1397]
1297
1398
1399+[[package]]
1400+name = "windows_aarch64_msvc"
1401+version = "0.32.0"
1402+source = "registry+https://github.com/rust-lang/crates.io-index"
1403+checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
1404+
1298[[package]]
1405[[package]]
1299name = "windows_aarch64_msvc"
1406name = "windows_aarch64_msvc"
1300version = "0.36.1"
1407version = "0.36.1"
1301source = "registry+https://github.com/rust-lang/crates.io-index"
1408source = "registry+https://github.com/rust-lang/crates.io-index"
1302checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
1409checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
1303
1410
1411+[[package]]
1412+name = "windows_i686_gnu"
1413+version = "0.32.0"
1414+source = "registry+https://github.com/rust-lang/crates.io-index"
1415+checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
1416+
1304[[package]]
1417[[package]]
1305name = "windows_i686_gnu"
1418name = "windows_i686_gnu"
1306version = "0.36.1"
1419version = "0.36.1"
1307source = "registry+https://github.com/rust-lang/crates.io-index"
1420source = "registry+https://github.com/rust-lang/crates.io-index"
1308checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
1421checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
1309
1422
1423+[[package]]
1424+name = "windows_i686_msvc"
1425+version = "0.32.0"
1426+source = "registry+https://github.com/rust-lang/crates.io-index"
1427+checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
1428+
1310[[package]]
1429[[package]]
1311name = "windows_i686_msvc"
1430name = "windows_i686_msvc"
1312version = "0.36.1"
1431version = "0.36.1"
1313source = "registry+https://github.com/rust-lang/crates.io-index"
1432source = "registry+https://github.com/rust-lang/crates.io-index"
1314checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
1433checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
1315
1434
1435+[[package]]
1436+name = "windows_x86_64_gnu"
1437+version = "0.32.0"
1438+source = "registry+https://github.com/rust-lang/crates.io-index"
1439+checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
1440+
1316[[package]]
1441[[package]]
1317name = "windows_x86_64_gnu"
1442name = "windows_x86_64_gnu"
1318version = "0.36.1"
1443version = "0.36.1"
1319source = "registry+https://github.com/rust-lang/crates.io-index"
1444source = "registry+https://github.com/rust-lang/crates.io-index"
1320checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
1445checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
1321
1446
1447+[[package]]
1448+name = "windows_x86_64_msvc"
1449+version = "0.32.0"
1450+source = "registry+https://github.com/rust-lang/crates.io-index"
1451+checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
1452+
1322[[package]]
1453[[package]]
1323name = "windows_x86_64_msvc"
1454name = "windows_x86_64_msvc"
1324version = "0.36.1"
1455version = "0.36.1"
docs/CONTRIBUTING.md+7
@@ -35,3 +35,10 @@ to `cargo install` anything either).
35[architecture.md]: ./architecture.md
35[architecture.md]: ./architecture.md
36[docs]: https://docs.helix-editor.com/
36[docs]: https://docs.helix-editor.com/
37[xtask]: https://github.com/matklad/cargo-xtask
37[xtask]: https://github.com/matklad/cargo-xtask
38+
39+# Integration tests
40+
41+Integration tests for helix-term can be run with `cargo integration-test`. Code
42+contributors are strongly encouraged to write integration tests for their code.
43+Existing tests can be used as examples. Helpers can be found in
44+[helpers.rs][../helix-term/tests/test/helpers.rs].
helix-core/Cargo.toml+1
@@ -12,6 +12,7 @@ include = ["src/**/*", "README.md"]
12
12
13[features]
13[features]
14unicode-lines = ["ropey/unicode_lines"]
14unicode-lines = ["ropey/unicode_lines"]
15+integration = []
15
16
16[dependencies]
17[dependencies]
17helix-loader = { version = "0.6", path = "../helix-loader" }
18helix-loader = { version = "0.6", path = "../helix-loader" }
helix-core/src/auto_pairs.rs+10−19
@@ -1,12 +1,9 @@
1//! When typing the opening character of one of the possible pairs defined below,
1//! When typing the opening character of one of the possible pairs defined below,
2//! this module provides the functionality to insert the paired closing character.
2//! this module provides the functionality to insert the paired closing character.
3
3
4−use crate::{
5− graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
4+use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
6−};
7use std::collections::HashMap;
5use std::collections::HashMap;
8
6
9−use log::debug;
10use smallvec::SmallVec;
7use smallvec::SmallVec;
11
8
12// Heavily based on https://github.com/codemirror/closebrackets/
9// Heavily based on https://github.com/codemirror/closebrackets/
@@ -125,7 +122,7 @@ impl Default for AutoPairs {
125
122
126#[must_use]
123#[must_use]
127pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
124pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
128− debug!("autopairs hook selection: {:#?}", selection);
125+ log::trace!("autopairs hook selection: {:#?}", selection);
129
126
130 if let Some(pair) = pairs.get(ch) {
127 if let Some(pair) = pairs.get(ch) {
131 if pair.same() {
128 if pair.same() {
@@ -149,14 +146,6 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
149 doc.get_char(pos - 1)
146 doc.get_char(pos - 1)
150}
147}
151
148
152−fn is_single_grapheme(doc: &Rope, range: &Range) -> bool {
153− let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
154− let first = graphemes.next();
155− let second = graphemes.next();
156− debug!("first: {:#?}, second: {:#?}", first, second);
157− first.is_some() && second.is_none()
158−}
159−
160/// calculate what the resulting range should be for an auto pair insertion
149/// calculate what the resulting range should be for an auto pair insertion
161fn get_next_range(
150fn get_next_range(
162 doc: &Rope,
151 doc: &Rope,
@@ -189,8 +178,8 @@ fn get_next_range(
189 );
178 );
190 }
179 }
191
180
192− let single_grapheme = is_single_grapheme(doc, start_range);
193 let doc_slice = doc.slice(..);
181 let doc_slice = doc.slice(..);
182+ let single_grapheme = start_range.is_single_grapheme(doc_slice);
194
183
195 // just skip over graphemes
184 // just skip over graphemes
196 if len_inserted == 0 {
185 if len_inserted == 0 {
@@ -235,9 +224,11 @@ fn get_next_range(
235 // other end of the grapheme to get to where the new characters
224 // other end of the grapheme to get to where the new characters
236 // are inserted, then move the head to where it should be
225 // are inserted, then move the head to where it should be
237 let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
226 let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
238− debug!(
227+ log::trace!(
239 "prev_bound: {}, offset: {}, len_inserted: {}",
228 "prev_bound: {}, offset: {}, len_inserted: {}",
229+ prev_bound,
230+ offset,
240− prev_bound, offset, len_inserted
231+ len_inserted
241 );
232 );
242 prev_bound + offset + len_inserted
233 prev_bound + offset + len_inserted
243 };
234 };
@@ -312,7 +303,7 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
312 });
303 });
313
304
314 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
305 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
315− debug!("auto pair transaction: {:#?}", t);
306+ log::debug!("auto pair transaction: {:#?}", t);
316 t
307 t
317}
308}
318
309
@@ -344,7 +335,7 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
344 });
335 });
345
336
346 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
337 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
347− debug!("auto pair transaction: {:#?}", t);
338+ log::debug!("auto pair transaction: {:#?}", t);
348 t
339 t
349}
340}
350
341
@@ -384,7 +375,7 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
384 });
375 });
385
376
386 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
377 let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
387− debug!("auto pair transaction: {:#?}", t);
378+ log::debug!("auto pair transaction: {:#?}", t);
388 t
379 t
389}
380}
390
381
helix-core/src/selection.rs+9−1
@@ -8,7 +8,7 @@ use crate::{
8 prev_grapheme_boundary,
8 prev_grapheme_boundary,
9 },
9 },
10 movement::Direction,
10 movement::Direction,
11− Assoc, ChangeSet, RopeSlice,
11+ Assoc, ChangeSet, RopeGraphemes, RopeSlice,
12};
12};
13use smallvec::{smallvec, SmallVec};
13use smallvec::{smallvec, SmallVec};
14use std::borrow::Cow;
14use std::borrow::Cow;
@@ -339,6 +339,14 @@ impl Range {
339 pub fn cursor_line(&self, text: RopeSlice) -> usize {
339 pub fn cursor_line(&self, text: RopeSlice) -> usize {
340 text.char_to_line(self.cursor(text))
340 text.char_to_line(self.cursor(text))
341 }
341 }
342+
343+ /// Returns true if this Range covers a single grapheme in the given text
344+ pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool {
345+ let mut graphemes = RopeGraphemes::new(doc.slice(self.from()..self.to()));
346+ let first = graphemes.next();
347+ let second = graphemes.next();
348+ first.is_some() && second.is_none()
349+ }
342}
350}
343
351
344impl From<(usize, usize)> for Range {
352impl From<(usize, usize)> for Range {
helix-loader/Cargo.toml−1
@@ -20,7 +20,6 @@ toml = "0.5"
20etcetera = "0.4"
20etcetera = "0.4"
21tree-sitter = "0.20"
21tree-sitter = "0.20"
22once_cell = "1.12"
22once_cell = "1.12"
23−
24log = "0.4"
23log = "0.4"
25
24
26# TODO: these two should be on !wasm32 only
25# TODO: these two should be on !wasm32 only
helix-loader/src/lib.rs+7−5
@@ -11,17 +11,19 @@ pub fn runtime_dir() -> std::path::PathBuf {
11 return dir.into();
11 return dir.into();
12 }
12 }
13
13
14+ if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
15+ // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
16+ let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
17+ log::debug!("runtime dir: {}", path.to_string_lossy());
18+ return path;
19+ }
20+
14 const RT_DIR: &str = "runtime";
21 const RT_DIR: &str = "runtime";
15 let conf_dir = config_dir().join(RT_DIR);
22 let conf_dir = config_dir().join(RT_DIR);
16 if conf_dir.exists() {
23 if conf_dir.exists() {
17 return conf_dir;
24 return conf_dir;
18 }
25 }
19
26
20− if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
21− // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
22− return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
23− }
24−
25 // fallback to location of the executable being run
27 // fallback to location of the executable being run
26 std::env::current_exe()
28 std::env::current_exe()
27 .ok()
29 .ok()
helix-term/Cargo.toml+6
@@ -17,6 +17,7 @@ app = true
17
17
18[features]
18[features]
19unicode-lines = ["helix-core/unicode-lines"]
19unicode-lines = ["helix-core/unicode-lines"]
20+integration = []
20
21
21[[bin]]
22[[bin]]
22name = "hx"
23name = "hx"
@@ -73,3 +74,8 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
73
74
74[build-dependencies]
75[build-dependencies]
75helix-loader = { version = "0.6", path = "../helix-loader" }
76helix-loader = { version = "0.6", path = "../helix-loader" }
77+
78+[dev-dependencies]
79+smallvec = "1.8"
80+indoc = "1.0.3"
81+tempfile = "3.3.0"
helix-term/src/application.rs+105−52
@@ -1,4 +1,5 @@
1use arc_swap::{access::Map, ArcSwap};
1use arc_swap::{access::Map, ArcSwap};
2+use futures_util::Stream;
2use helix_core::{
3use helix_core::{
3 config::{default_syntax_loader, user_syntax_loader},
4 config::{default_syntax_loader, user_syntax_loader},
4 pos_at_coords, syntax, Selection,
5 pos_at_coords, syntax, Selection,
@@ -24,10 +25,10 @@ use std::{
24 time::{Duration, Instant},
25 time::{Duration, Instant},
25};
26};
26
27
27−use anyhow::Error;
28+use anyhow::{Context, Error};
28
29
29use crossterm::{
30use crossterm::{
30− event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
31+ event::{DisableMouseCapture, EnableMouseCapture, Event},
31 execute, terminal,
32 execute, terminal,
32 tty::IsTty,
33 tty::IsTty,
33};
34};
@@ -39,9 +40,11 @@ use {
39#[cfg(windows)]
40#[cfg(windows)]
40type Signals = futures_util::stream::Empty<()>;
41type Signals = futures_util::stream::Empty<()>;
41
42
43+const LSP_DEADLINE: Duration = Duration::from_millis(16);
44+
42pub struct Application {
45pub struct Application {
43 compositor: Compositor,
46 compositor: Compositor,
44− editor: Editor,
47+ pub editor: Editor,
45
48
46 config: Arc<ArcSwap<Config>>,
49 config: Arc<ArcSwap<Config>>,
47
50
@@ -53,32 +56,39 @@ pub struct Application {
53 signals: Signals,
56 signals: Signals,
54 jobs: Jobs,
57 jobs: Jobs,
55 lsp_progress: LspProgressMap,
58 lsp_progress: LspProgressMap,
59+ last_render: Instant,
60+}
61+
62+#[cfg(feature = "integration")]
63+fn setup_integration_logging() {
64+ let level = std::env::var("HELIX_LOG_LEVEL")
65+ .map(|lvl| lvl.parse().unwrap())
66+ .unwrap_or(log::LevelFilter::Info);
67+
68+ // Separate file config so we can include year, month and day in file logs
69+ let _ = fern::Dispatch::new()
70+ .format(|out, message, record| {
71+ out.finish(format_args!(
72+ "{} {} [{}] {}",
73+ chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"),
74+ record.target(),
75+ record.level(),
76+ message
77+ ))
78+ })
79+ .level(level)
80+ .chain(std::io::stdout())
81+ .apply();
56}
82}
57
83
58impl Application {
84impl Application {
59− pub fn new(args: Args) -> Result<Self, Error> {
85+ pub fn new(args: Args, config: Config) -> Result<Self, Error> {
86+ #[cfg(feature = "integration")]
87+ setup_integration_logging();
88+
60 use helix_view::editor::Action;
89 use helix_view::editor::Action;
61
90
62 let config_dir = helix_loader::config_dir();
91 let config_dir = helix_loader::config_dir();
63− if !config_dir.exists() {
64− std::fs::create_dir_all(&config_dir).ok();
65− }
66−
67− let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
68− Ok(config) => toml::from_str(&config)
69− .map(crate::keymap::merge_keys)
70− .unwrap_or_else(|err| {
71− eprintln!("Bad config: {}", err);
72− eprintln!("Press <ENTER> to continue with default config");
73− use std::io::Read;
74− // This waits for an enter press.
75− let _ = std::io::stdin().read(&mut []);
76− Config::default()
77− }),
78− Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
79− Err(err) => return Err(Error::new(err)),
80− };
81−
82 let theme_loader = std::sync::Arc::new(theme::Loader::new(
92 let theme_loader = std::sync::Arc::new(theme::Loader::new(
83 &config_dir,
93 &config_dir,
84 &helix_loader::runtime_dir(),
94 &helix_loader::runtime_dir(),
@@ -116,7 +126,7 @@ impl Application {
116 });
126 });
117 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
127 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
118
128
119− let mut compositor = Compositor::new()?;
129+ let mut compositor = Compositor::new().context("build compositor")?;
120 let config = Arc::new(ArcSwap::from_pointee(config));
130 let config = Arc::new(ArcSwap::from_pointee(config));
121 let mut editor = Editor::new(
131 let mut editor = Editor::new(
122 compositor.size(),
132 compositor.size(),
@@ -135,26 +145,28 @@ impl Application {
135
145
136 if args.load_tutor {
146 if args.load_tutor {
137 let path = helix_loader::runtime_dir().join("tutor.txt");
147 let path = helix_loader::runtime_dir().join("tutor.txt");
138− editor.open(path, Action::VerticalSplit)?;
148+ editor.open(&path, Action::VerticalSplit)?;
139 // Unset path to prevent accidentally saving to the original tutor file.
149 // Unset path to prevent accidentally saving to the original tutor file.
140 doc_mut!(editor).set_path(None)?;
150 doc_mut!(editor).set_path(None)?;
141 } else if !args.files.is_empty() {
151 } else if !args.files.is_empty() {
142 let first = &args.files[0].0; // we know it's not empty
152 let first = &args.files[0].0; // we know it's not empty
143 if first.is_dir() {
153 if first.is_dir() {
144− std::env::set_current_dir(&first)?;
154+ std::env::set_current_dir(&first).context("set current dir")?;
145 editor.new_file(Action::VerticalSplit);
155 editor.new_file(Action::VerticalSplit);
146 let picker = ui::file_picker(".".into(), &config.load().editor);
156 let picker = ui::file_picker(".".into(), &config.load().editor);
147 compositor.push(Box::new(overlayed(picker)));
157 compositor.push(Box::new(overlayed(picker)));
148 } else {
158 } else {
149 let nr_of_files = args.files.len();
159 let nr_of_files = args.files.len();
150− editor.open(first.to_path_buf(), Action::VerticalSplit)?;
160+ editor.open(first, Action::VerticalSplit)?;
151 for (file, pos) in args.files {
161 for (file, pos) in args.files {
152 if file.is_dir() {
162 if file.is_dir() {
153 return Err(anyhow::anyhow!(
163 return Err(anyhow::anyhow!(
154 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
164 "expected a path to file, found a directory. (to open a directory pass it as first argument)"
155 ));
165 ));
156 } else {
166 } else {
167+ let doc_id = editor
157− let doc_id = editor.open(file, Action::Load)?;
168+ .open(&file, Action::Load)
169+ .context(format!("open '{}'", file.to_string_lossy()))?;
158 // with Action::Load all documents have the same view
170 // with Action::Load all documents have the same view
159 let view_id = editor.tree.focus;
171 let view_id = editor.tree.focus;
160 let doc = editor.document_mut(doc_id).unwrap();
172 let doc = editor.document_mut(doc_id).unwrap();
@@ -168,7 +180,7 @@ impl Application {
168 let (view, doc) = current!(editor);
180 let (view, doc) = current!(editor);
169 align_view(doc, view, Align::Center);
181 align_view(doc, view, Align::Center);
170 }
182 }
171− } else if stdin().is_tty() {
183+ } else if stdin().is_tty() || cfg!(feature = "integration") {
172 editor.new_file(Action::VerticalSplit);
184 editor.new_file(Action::VerticalSplit);
173 } else if cfg!(target_os = "macos") {
185 } else if cfg!(target_os = "macos") {
174 // On Linux and Windows, we allow the output of a command to be piped into the new buffer.
186 // On Linux and Windows, we allow the output of a command to be piped into the new buffer.
@@ -186,7 +198,8 @@ impl Application {
186 #[cfg(windows)]
198 #[cfg(windows)]
187 let signals = futures_util::stream::empty();
199 let signals = futures_util::stream::empty();
188 #[cfg(not(windows))]
200 #[cfg(not(windows))]
201+ let signals =
189− let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
202+ Signals::new(&[signal::SIGTSTP, signal::SIGCONT]).context("build signal handler")?;
190
203
191 let app = Self {
204 let app = Self {
192 compositor,
205 compositor,
@@ -200,40 +213,57 @@ impl Application {
200 signals,
213 signals,
201 jobs: Jobs::new(),
214 jobs: Jobs::new(),
202 lsp_progress: LspProgressMap::new(),
215 lsp_progress: LspProgressMap::new(),
216+ last_render: Instant::now(),
203 };
217 };
204
218
205 Ok(app)
219 Ok(app)
206 }
220 }
207
221
208 fn render(&mut self) {
222 fn render(&mut self) {
223+ let compositor = &mut self.compositor;
224+
209 let mut cx = crate::compositor::Context {
225 let mut cx = crate::compositor::Context {
210 editor: &mut self.editor,
226 editor: &mut self.editor,
211 jobs: &mut self.jobs,
227 jobs: &mut self.jobs,
212 scroll: None,
228 scroll: None,
213 };
229 };
214
230
215− self.compositor.render(&mut cx);
231+ compositor.render(&mut cx);
216 }
232 }
217
233
218− pub async fn event_loop(&mut self) {
234+ pub async fn event_loop<S>(&mut self, input_stream: &mut S)
219− let mut reader = EventStream::new();
235+ where
220− let mut last_render = Instant::now();
236+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
221− let deadline = Duration::from_secs(1) / 60;
237+ {
222−
223 self.render();
238 self.render();
239+ self.last_render = Instant::now();
224
240
225 loop {
241 loop {
226− if self.editor.should_close() {
242+ if !self.event_loop_until_idle(input_stream).await {
227 break;
243 break;
228 }
244 }
245+ }
246+ }
247+
248+ pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
249+ where
250+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
251+ {
252+ #[cfg(feature = "integration")]
253+ let mut idle_handled = false;
254+
255+ loop {
256+ if self.editor.should_close() {
257+ return false;
258+ }
229
259
230 use futures_util::StreamExt;
260 use futures_util::StreamExt;
231
261
232 tokio::select! {
262 tokio::select! {
233 biased;
263 biased;
234
264
235− event = reader.next() => {
265+ Some(event) = input_stream.next() => {
236− self.handle_terminal_events(event)
266+ self.handle_terminal_events(event);
237 }
267 }
238 Some(signal) = self.signals.next() => {
268 Some(signal) = self.signals.next() => {
239 self.handle_signals(signal).await;
269 self.handle_signals(signal).await;
@@ -242,9 +272,10 @@ impl Application {
242 self.handle_language_server_message(call, id).await;
272 self.handle_language_server_message(call, id).await;
243 // limit render calls for fast language server messages
273 // limit render calls for fast language server messages
244 let last = self.editor.language_servers.incoming.is_empty();
274 let last = self.editor.language_servers.incoming.is_empty();
275+
245− if last || last_render.elapsed() > deadline {
276+ if last || self.last_render.elapsed() > LSP_DEADLINE {
246 self.render();
277 self.render();
247− last_render = Instant::now();
278+ self.last_render = Instant::now();
248 }
279 }
249 }
280 }
250 Some(payload) = self.editor.debugger_events.next() => {
281 Some(payload) = self.editor.debugger_events.next() => {
@@ -269,8 +300,24 @@ impl Application {
269 // idle timeout
300 // idle timeout
270 self.editor.clear_idle_timer();
301 self.editor.clear_idle_timer();
271 self.handle_idle_timeout();
302 self.handle_idle_timeout();
303+
304+ #[cfg(feature = "integration")]
305+ {
306+ idle_handled = true;
307+ }
272 }
308 }
273 }
309 }
310+
311+ // for integration tests only, reset the idle timer after every
312+ // event to make a signal when test events are done processing
313+ #[cfg(feature = "integration")]
314+ {
315+ if idle_handled {
316+ return true;
317+ }
318+
319+ self.editor.reset_idle_timer();
320+ }
274 }
321 }
275 }
322 }
276
323
@@ -370,7 +417,7 @@ impl Application {
370 }
417 }
371 }
418 }
372
419
373− pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
420+ pub fn handle_terminal_events(&mut self, event: Result<Event, crossterm::ErrorKind>) {
374 let mut cx = crate::compositor::Context {
421 let mut cx = crate::compositor::Context {
375 editor: &mut self.editor,
422 editor: &mut self.editor,
376 jobs: &mut self.jobs,
423 jobs: &mut self.jobs,
@@ -378,15 +425,14 @@ impl Application {
378 };
425 };
379 // Handle key events
426 // Handle key events
380 let should_redraw = match event {
427 let should_redraw = match event {
381− Some(Ok(Event::Resize(width, height))) => {
428+ Ok(Event::Resize(width, height)) => {
382 self.compositor.resize(width, height);
429 self.compositor.resize(width, height);
383
430
384 self.compositor
431 self.compositor
385 .handle_event(Event::Resize(width, height), &mut cx)
432 .handle_event(Event::Resize(width, height), &mut cx)
386 }
433 }
387− Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
434+ Ok(event) => self.compositor.handle_event(event, &mut cx),
388− Some(Err(x)) => panic!("{}", x),
435+ Err(x) => panic!("{}", x),
389− None => panic!(),
390 };
436 };
391
437
392 if should_redraw && !self.editor.should_close() {
438 if should_redraw && !self.editor.should_close() {
@@ -740,7 +786,10 @@ impl Application {
740 Ok(())
786 Ok(())
741 }
787 }
742
788
743− pub async fn run(&mut self) -> Result<i32, Error> {
789+ pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
790+ where
791+ S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
792+ {
744 self.claim_term().await?;
793 self.claim_term().await?;
745
794
746 // Exit the alternate screen and disable raw mode before panicking
795 // Exit the alternate screen and disable raw mode before panicking
@@ -755,16 +804,20 @@ impl Application {
755 hook(info);
804 hook(info);
756 }));
805 }));
757
806
758− self.event_loop().await;
807+ self.event_loop(input_stream).await;
808+ self.close().await?;
809+ self.restore_term()?;
810+
811+ Ok(self.editor.exit_code)
812+ }
759
813
814+ pub async fn close(&mut self) -> anyhow::Result<()> {
760− self.jobs.finish().await;
815+ self.jobs.finish().await?;
761
816
762 if self.editor.close_language_servers(None).await.is_err() {
817 if self.editor.close_language_servers(None).await.is_err() {
763 log::error!("Timed out waiting for language servers to shutdown");
818 log::error!("Timed out waiting for language servers to shutdown");
764 };
819 };
765
820
766− self.restore_term()?;
821+ Ok(())
767−
768− Ok(self.editor.exit_code)
769 }
822 }
770}
823}
helix-term/src/commands.rs+10−3
@@ -1026,7 +1026,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
1026 for sel in paths {
1026 for sel in paths {
1027 let p = sel.trim();
1027 let p = sel.trim();
1028 if !p.is_empty() {
1028 if !p.is_empty() {
1029− if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
1029+ if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
1030 cx.editor.set_error(format!("Open file failed: {:?}", e));
1030 cx.editor.set_error(format!("Open file failed: {:?}", e));
1031 }
1031 }
1032 }
1032 }
@@ -1855,7 +1855,7 @@ fn global_search(cx: &mut Context) {
1855 }
1855 }
1856 },
1856 },
1857 move |cx, (line_num, path), action| {
1857 move |cx, (line_num, path), action| {
1858− match cx.editor.open(path.into(), action) {
1858+ match cx.editor.open(path, action) {
1859 Ok(_) => {}
1859 Ok(_) => {}
1860 Err(e) => {
1860 Err(e) => {
1861 cx.editor.set_error(format!(
1861 cx.editor.set_error(format!(
@@ -2100,10 +2100,17 @@ fn insert_mode(cx: &mut Context) {
2100 let (view, doc) = current!(cx.editor);
2100 let (view, doc) = current!(cx.editor);
2101 enter_insert_mode(doc);
2101 enter_insert_mode(doc);
2102
2102
2103+ log::trace!(
2104+ "entering insert mode with sel: {:?}, text: {:?}",
2105+ doc.selection(view.id),
2106+ doc.text().to_string()
2107+ );
2108+
2103 let selection = doc
2109 let selection = doc
2104 .selection(view.id)
2110 .selection(view.id)
2105 .clone()
2111 .clone()
2106 .transform(|range| Range::new(range.to(), range.from()));
2112 .transform(|range| Range::new(range.to(), range.from()));
2113+
2107 doc.set_selection(view.id, selection);
2114 doc.set_selection(view.id, selection);
2108}
2115}
2109
2116
@@ -2449,8 +2456,8 @@ fn normal_mode(cx: &mut Context) {
2449 graphemes::prev_grapheme_boundary(text, range.to()),
2456 graphemes::prev_grapheme_boundary(text, range.to()),
2450 )
2457 )
2451 });
2458 });
2452− doc.set_selection(view.id, selection);
2453
2459
2460+ doc.set_selection(view.id, selection);
2454 doc.restore_cursor = false;
2461 doc.restore_cursor = false;
2455 }
2462 }
2456}
2463}
helix-term/src/commands/lsp.rs+3−3
@@ -61,7 +61,7 @@ fn jump_to_location(
61 return;
61 return;
62 }
62 }
63 };
63 };
64− let _id = editor.open(path, action).expect("editor.open failed");
64+ let _id = editor.open(&path, action).expect("editor.open failed");
65 let (view, doc) = current!(editor);
65 let (view, doc) = current!(editor);
66 let definition_pos = location.range.start;
66 let definition_pos = location.range.start;
67 // TODO: convert inside server
67 // TODO: convert inside server
@@ -114,7 +114,7 @@ fn sym_picker(
114 return;
114 return;
115 }
115 }
116 };
116 };
117− if let Err(err) = cx.editor.open(path, action) {
117+ if let Err(err) = cx.editor.open(&path, action) {
118 let err = format!("failed to open document: {}: {}", uri, err);
118 let err = format!("failed to open document: {}: {}", uri, err);
119 log::error!("{}", err);
119 log::error!("{}", err);
120 cx.editor.set_error(err);
120 cx.editor.set_error(err);
@@ -383,7 +383,7 @@ pub fn apply_workspace_edit(
383 };
383 };
384
384
385 let current_view_id = view!(editor).id;
385 let current_view_id = view!(editor).id;
386− let doc_id = match editor.open(path, Action::Load) {
386+ let doc_id = match editor.open(&path, Action::Load) {
387 Ok(doc_id) => doc_id,
387 Ok(doc_id) => doc_id,
388 Err(err) => {
388 Err(err) => {
389 let err = format!("failed to open document: {}: {}", uri, err);
389 let err = format!("failed to open document: {}: {}", uri, err);
helix-term/src/commands/typed.rs+8−6
@@ -50,7 +50,7 @@ fn open(
50 ensure!(!args.is_empty(), "wrong argument count");
50 ensure!(!args.is_empty(), "wrong argument count");
51 for arg in args {
51 for arg in args {
52 let (path, pos) = args::parse_file(arg);
52 let (path, pos) = args::parse_file(arg);
53− let _ = cx.editor.open(path, Action::Replace)?;
53+ let _ = cx.editor.open(&path, Action::Replace)?;
54 let (view, doc) = current!(cx.editor);
54 let (view, doc) = current!(cx.editor);
55 let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
55 let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
56 doc.set_selection(view.id, pos);
56 doc.set_selection(view.id, pos);
@@ -233,6 +233,7 @@ fn write_impl(
233 doc.detect_language(cx.editor.syn_loader.clone());
233 doc.detect_language(cx.editor.syn_loader.clone());
234 let _ = cx.editor.refresh_language_server(id);
234 let _ = cx.editor.refresh_language_server(id);
235 }
235 }
236+
236 Ok(())
237 Ok(())
237}
238}
238
239
@@ -422,6 +423,7 @@ fn write_quit(
422 event: PromptEvent,
423 event: PromptEvent,
423) -> anyhow::Result<()> {
424) -> anyhow::Result<()> {
424 write_impl(cx, args.first(), false)?;
425 write_impl(cx, args.first(), false)?;
426+ helix_lsp::block_on(cx.jobs.finish())?;
425 quit(cx, &[], event)
427 quit(cx, &[], event)
426}
428}
427
429
@@ -819,7 +821,7 @@ fn vsplit(
819 } else {
821 } else {
820 for arg in args {
822 for arg in args {
821 cx.editor
823 cx.editor
822− .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
824+ .open(&PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
823 }
825 }
824 }
826 }
825
827
@@ -838,7 +840,7 @@ fn hsplit(
838 } else {
840 } else {
839 for arg in args {
841 for arg in args {
840 cx.editor
842 cx.editor
841− .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
843+ .open(&PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
842 }
844 }
843 }
845 }
844
846
@@ -923,7 +925,7 @@ fn tutor(
923 _event: PromptEvent,
925 _event: PromptEvent,
924) -> anyhow::Result<()> {
926) -> anyhow::Result<()> {
925 let path = helix_loader::runtime_dir().join("tutor.txt");
927 let path = helix_loader::runtime_dir().join("tutor.txt");
926− cx.editor.open(path, Action::Replace)?;
928+ cx.editor.open(&path, Action::Replace)?;
927 // Unset path to prevent accidentally saving to the original tutor file.
929 // Unset path to prevent accidentally saving to the original tutor file.
928 doc_mut!(cx.editor).set_path(None)?;
930 doc_mut!(cx.editor).set_path(None)?;
929 Ok(())
931 Ok(())
@@ -1150,7 +1152,7 @@ fn open_config(
1150 _event: PromptEvent,
1152 _event: PromptEvent,
1151) -> anyhow::Result<()> {
1153) -> anyhow::Result<()> {
1152 cx.editor
1154 cx.editor
1153− .open(helix_loader::config_file(), Action::Replace)?;
1155+ .open(&helix_loader::config_file(), Action::Replace)?;
1154 Ok(())
1156 Ok(())
1155}
1157}
1156
1158
@@ -1159,7 +1161,7 @@ fn open_log(
1159 _args: &[Cow<str>],
1161 _args: &[Cow<str>],
1160 _event: PromptEvent,
1162 _event: PromptEvent,
1161) -> anyhow::Result<()> {
1163) -> anyhow::Result<()> {
1162− cx.editor.open(helix_loader::log_file(), Action::Replace)?;
1164+ cx.editor.open(&helix_loader::log_file(), Action::Replace)?;
1163 Ok(())
1165 Ok(())
1164}
1166}
1165
1167
helix-term/src/compositor.rs+22−4
@@ -5,6 +5,9 @@ use helix_core::Position;
5use helix_view::graphics::{CursorKind, Rect};
5use helix_view::graphics::{CursorKind, Rect};
6
6
7use crossterm::event::Event;
7use crossterm::event::Event;
8+
9+#[cfg(feature = "integration")]
10+use tui::backend::TestBackend;
8use tui::buffer::Buffer as Surface;
11use tui::buffer::Buffer as Surface;
9
12
10pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
13pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
@@ -63,11 +66,21 @@ pub trait Component: Any + AnyComponent {
63 }
66 }
64}
67}
65
68
66−use anyhow::Error;
69+use anyhow::Context as AnyhowContext;
70+use tui::backend::Backend;
71+
72+#[cfg(not(feature = "integration"))]
73+use tui::backend::CrosstermBackend;
74+
75+#[cfg(not(feature = "integration"))]
67use std::io::stdout;
76use std::io::stdout;
68−use tui::backend::{Backend, CrosstermBackend};
77+
78+#[cfg(not(feature = "integration"))]
69type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
79type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
70
80
81+#[cfg(feature = "integration")]
82+type Terminal = tui::terminal::Terminal<TestBackend>;
83+
71pub struct Compositor {
84pub struct Compositor {
72 layers: Vec<Box<dyn Component>>,
85 layers: Vec<Box<dyn Component>>,
73 terminal: Terminal,
86 terminal: Terminal,
@@ -76,9 +89,14 @@ pub struct Compositor {
76}
89}
77
90
78impl Compositor {
91impl Compositor {
79− pub fn new() -> Result<Self, Error> {
92+ pub fn new() -> anyhow::Result<Self> {
93+ #[cfg(not(feature = "integration"))]
80 let backend = CrosstermBackend::new(stdout());
94 let backend = CrosstermBackend::new(stdout());
95+
96+ #[cfg(feature = "integration")]
97+ let backend = TestBackend::new(120, 150);
98+
81− let terminal = Terminal::new(backend)?;
99+ let terminal = Terminal::new(backend).context("build terminal")?;
82 Ok(Self {
100 Ok(Self {
83 layers: Vec::new(),
101 layers: Vec::new(),
84 terminal,
102 terminal,
helix-term/src/job.rs+17−4
@@ -2,7 +2,7 @@ use helix_view::Editor;
2
2
3use crate::compositor::Compositor;
3use crate::compositor::Compositor;
4
4
5−use futures_util::future::{self, BoxFuture, Future, FutureExt};
5+use futures_util::future::{BoxFuture, Future, FutureExt};
6use futures_util::stream::{FuturesUnordered, StreamExt};
6use futures_util::stream::{FuturesUnordered, StreamExt};
7
7
8pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
8pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
@@ -93,8 +93,21 @@ impl Jobs {
93 }
93 }
94
94
95 /// Blocks until all the jobs that need to be waited on are done.
95 /// Blocks until all the jobs that need to be waited on are done.
96− pub async fn finish(&mut self) {
96+ pub async fn finish(&mut self) -> anyhow::Result<()> {
97+ log::debug!("waiting on jobs...");
97− let wait_futures = std::mem::take(&mut self.wait_futures);
98+ let mut wait_futures = std::mem::take(&mut self.wait_futures);
98− wait_futures.for_each(|_| future::ready(())).await
99+ while let (Some(job), tail) = wait_futures.into_future().await {
100+ match job {
101+ Ok(_) => {
102+ wait_futures = tail;
103+ }
104+ Err(e) => {
105+ self.wait_futures = tail;
106+ return Err(e);
107+ }
108+ }
109+ }
110+
111+ Ok(())
99 }
112 }
100}
113}
helix-term/src/main.rs+25−3
@@ -1,6 +1,8 @@
1−use anyhow::{Context, Result};
1+use anyhow::{Context, Error, Result};
2+use crossterm::event::EventStream;
2use helix_term::application::Application;
3use helix_term::application::Application;
3use helix_term::args::Args;
4use helix_term::args::Args;
5+use helix_term::config::Config;
4use std::path::PathBuf;
6use std::path::PathBuf;
5
7
6fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
8fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -110,10 +112,30 @@ FLAGS:
110
112
111 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
113 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
112
114
115+ let config_dir = helix_loader::config_dir();
116+ if !config_dir.exists() {
117+ std::fs::create_dir_all(&config_dir).ok();
118+ }
119+
120+ let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
121+ Ok(config) => toml::from_str(&config)
122+ .map(helix_term::keymap::merge_keys)
123+ .unwrap_or_else(|err| {
124+ eprintln!("Bad config: {}", err);
125+ eprintln!("Press <ENTER> to continue with default config");
126+ use std::io::Read;
127+ // This waits for an enter press.
128+ let _ = std::io::stdin().read(&mut []);
129+ Config::default()
130+ }),
131+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
132+ Err(err) => return Err(Error::new(err)),
133+ };
134+
113 // TODO: use the thread local executor to spawn the application task separately from the work pool
135 // TODO: use the thread local executor to spawn the application task separately from the work pool
114− let mut app = Application::new(args).context("unable to create new application")?;
136+ let mut app = Application::new(args, config).context("unable to create new application")?;
115
137
116− let exit_code = app.run().await?;
138+ let exit_code = app.run(&mut EventStream::new()).await?;
117
139
118 Ok(exit_code)
140 Ok(exit_code)
119}
141}
helix-term/src/ui/mod.rs+1−1
@@ -175,7 +175,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
175 path.strip_prefix(&root).unwrap_or(path).to_string_lossy()
175 path.strip_prefix(&root).unwrap_or(path).to_string_lossy()
176 },
176 },
177 move |cx, path: &PathBuf, action| {
177 move |cx, path: &PathBuf, action| {
178− if let Err(e) = cx.editor.open(path.into(), action) {
178+ if let Err(e) = cx.editor.open(path, action) {
179 let err = if let Some(err) = e.source() {
179 let err = if let Some(err) = e.source() {
180 format!("{}", err)
180 format!("{}", err)
181 } else {
181 } else {
helix-term/tests/integration.rsnew file+25
@@ -0,0 +1,25 @@
1+#[cfg(feature = "integration")]
2+mod test {
3+ mod helpers;
4+
5+ use std::path::PathBuf;
6+
7+ use helix_core::{syntax::AutoPairConfig, Position, Selection};
8+ use helix_term::{args::Args, config::Config};
9+
10+ use indoc::indoc;
11+
12+ use self::helpers::*;
13+
14+ #[tokio::test]
15+ async fn hello_world() -> anyhow::Result<()> {
16+ test(("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#")).await?;
17+ Ok(())
18+ }
19+
20+ mod auto_indent;
21+ mod auto_pairs;
22+ mod commands;
23+ mod movement;
24+ mod write;
25+}
helix-term/tests/test/auto_indent.rsnew file+26
@@ -0,0 +1,26 @@
1+use super::*;
2+
3+#[tokio::test]
4+async fn auto_indent_c() -> anyhow::Result<()> {
5+ test_with_config(
6+ Args {
7+ files: vec![(PathBuf::from("foo.c"), Position::default())],
8+ ..Default::default()
9+ },
10+ Config::default(),
11+ // switches to append mode?
12+ (
13+ helpers::platform_line("void foo() {#[|}]#").as_ref(),
14+ "i<ret><esc>",
15+ helpers::platform_line(indoc! {"\
16+ void foo() {
17+ #[|\n]#\
18+ }
19+ "})
20+ .as_ref(),
21+ ),
22+ )
23+ .await?;
24+
25+ Ok(())
26+}
helix-term/tests/test/auto_pairs.rsnew file+21
@@ -0,0 +1,21 @@
1+use super::*;
2+
3+#[tokio::test]
4+async fn auto_pairs_basic() -> anyhow::Result<()> {
5+ test(("#[\n|]#", "i(<esc>", "(#[|)]#\n")).await?;
6+
7+ test_with_config(
8+ Args::default(),
9+ Config {
10+ editor: helix_view::editor::Config {
11+ auto_pairs: AutoPairConfig::Enable(false),
12+ ..Default::default()
13+ },
14+ ..Default::default()
15+ },
16+ ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
17+ )
18+ .await?;
19+
20+ Ok(())
21+}
helix-term/tests/test/commands.rsnew file+93
@@ -0,0 +1,93 @@
1+use std::{
2+ io::{Read, Write},
3+ ops::RangeInclusive,
4+};
5+
6+use helix_core::diagnostic::Severity;
7+use helix_term::application::Application;
8+
9+use super::*;
10+
11+#[tokio::test]
12+async fn test_write_quit_fail() -> anyhow::Result<()> {
13+ let file = helpers::new_readonly_tempfile()?;
14+
15+ test_key_sequence(
16+ &mut helpers::app_with_file(file.path())?,
17+ Some("ihello<esc>:wq<ret>"),
18+ Some(&|app| {
19+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
20+ }),
21+ false,
22+ )
23+ .await?;
24+
25+ Ok(())
26+}
27+
28+#[tokio::test]
29+#[ignore]
30+async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
31+ test_key_sequences(
32+ &mut Application::new(Args::default(), Config::default())?,
33+ vec![
34+ (
35+ None,
36+ Some(&|app| {
37+ assert_eq!(1, app.editor.documents().count());
38+ assert!(!app.editor.is_err());
39+ }),
40+ ),
41+ (
42+ Some("ihello<esc>:new<ret>"),
43+ Some(&|app| {
44+ assert_eq!(2, app.editor.documents().count());
45+ assert!(!app.editor.is_err());
46+ }),
47+ ),
48+ (
49+ Some(":buffer<minus>close<ret>"),
50+ Some(&|app| {
51+ assert_eq!(1, app.editor.documents().count());
52+ assert!(!app.editor.is_err());
53+ }),
54+ ),
55+ ],
56+ false,
57+ )
58+ .await?;
59+
60+ // verify if writes are queued up, it finishes them before closing the buffer
61+ let mut file = tempfile::NamedTempFile::new()?;
62+ let mut command = String::new();
63+ const RANGE: RangeInclusive<i32> = 1..=1000;
64+
65+ for i in RANGE {
66+ let cmd = format!("%c{}<esc>:w<ret>", i);
67+ command.push_str(&cmd);
68+ }
69+
70+ command.push_str(":buffer<minus>close<ret>");
71+
72+ test_key_sequence(
73+ &mut helpers::app_with_file(file.path())?,
74+ Some(&command),
75+ Some(&|app| {
76+ assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
77+
78+ let doc = app.editor.document_by_path(file.path());
79+ assert!(doc.is_none(), "found doc: {:?}", doc);
80+ }),
81+ false,
82+ )
83+ .await?;
84+
85+ file.as_file_mut().flush()?;
86+ file.as_file_mut().sync_all()?;
87+
88+ let mut file_content = String::new();
89+ file.as_file_mut().read_to_string(&mut file_content)?;
90+ assert_eq!(RANGE.end().to_string(), file_content);
91+
92+ Ok(())
93+}
helix-term/tests/test/helpers.rsnew file+213
@@ -0,0 +1,213 @@
1+use std::{io::Write, path::PathBuf, time::Duration};
2+
3+use anyhow::bail;
4+use crossterm::event::{Event, KeyEvent};
5+use helix_core::{test, Selection, Transaction};
6+use helix_term::{application::Application, args::Args, config::Config};
7+use helix_view::{doc, input::parse_macro};
8+use tempfile::NamedTempFile;
9+use tokio_stream::wrappers::UnboundedReceiverStream;
10+
11+#[derive(Clone, Debug)]
12+pub struct TestCase {
13+ pub in_text: String,
14+ pub in_selection: Selection,
15+ pub in_keys: String,
16+ pub out_text: String,
17+ pub out_selection: Selection,
18+}
19+
20+impl<S: Into<String>> From<(S, S, S)> for TestCase {
21+ fn from((input, keys, output): (S, S, S)) -> Self {
22+ let (in_text, in_selection) = test::print(&input.into());
23+ let (out_text, out_selection) = test::print(&output.into());
24+
25+ TestCase {
26+ in_text,
27+ in_selection,
28+ in_keys: keys.into(),
29+ out_text,
30+ out_selection,
31+ }
32+ }
33+}
34+
35+#[inline]
36+pub async fn test_key_sequence(
37+ app: &mut Application,
38+ in_keys: Option<&str>,
39+ test_fn: Option<&dyn Fn(&Application)>,
40+ should_exit: bool,
41+) -> anyhow::Result<()> {
42+ test_key_sequences(app, vec![(in_keys, test_fn)], should_exit).await
43+}
44+
45+#[allow(clippy::type_complexity)]
46+pub async fn test_key_sequences(
47+ app: &mut Application,
48+ inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>,
49+ should_exit: bool,
50+) -> anyhow::Result<()> {
51+ const TIMEOUT: Duration = Duration::from_millis(500);
52+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
53+ let mut rx_stream = UnboundedReceiverStream::new(rx);
54+ let num_inputs = inputs.len();
55+
56+ for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
57+ if let Some(in_keys) = in_keys {
58+ for key_event in parse_macro(in_keys)?.into_iter() {
59+ tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
60+ }
61+ }
62+
63+ let app_exited = !app.event_loop_until_idle(&mut rx_stream).await;
64+
65+ // the app should not exit from any test until the last one
66+ if i < num_inputs - 1 && app_exited {
67+ bail!("application exited before test function could run");
68+ }
69+
70+ // verify if it exited on the last iteration if it should have and
71+ // the inverse
72+ if i == num_inputs - 1 && app_exited != should_exit {
73+ bail!("expected app to exit: {} != {}", app_exited, should_exit);
74+ }
75+
76+ if let Some(test) = test_fn {
77+ test(app);
78+ };
79+ }
80+
81+ if !should_exit {
82+ for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
83+ tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
84+ }
85+
86+ let event_loop = app.event_loop(&mut rx_stream);
87+ tokio::time::timeout(TIMEOUT, event_loop).await?;
88+ }
89+
90+ app.close().await?;
91+
92+ Ok(())
93+}
94+
95+pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
96+ app: Option<Application>,
97+ test_case: T,
98+ test_fn: &dyn Fn(&Application),
99+ should_exit: bool,
100+) -> anyhow::Result<()> {
101+ let test_case = test_case.into();
102+ let mut app = match app {
103+ Some(app) => app,
104+ None => Application::new(Args::default(), Config::default())?,
105+ };
106+
107+ let (view, doc) = helix_view::current!(app.editor);
108+ let sel = doc.selection(view.id).clone();
109+
110+ // replace the initial text with the input text
111+ doc.apply(
112+ &Transaction::change_by_selection(doc.text(), &sel, |_| {
113+ (0, doc.text().len_chars(), Some((&test_case.in_text).into()))
114+ })
115+ .with_selection(test_case.in_selection.clone()),
116+ view.id,
117+ );
118+
119+ test_key_sequence(
120+ &mut app,
121+ Some(&test_case.in_keys),
122+ Some(test_fn),
123+ should_exit,
124+ )
125+ .await
126+}
127+
128+/// Use this for very simple test cases where there is one input
129+/// document, selection, and sequence of key presses, and you just
130+/// want to verify the resulting document and selection.
131+pub async fn test_with_config<T: Into<TestCase>>(
132+ args: Args,
133+ config: Config,
134+ test_case: T,
135+) -> anyhow::Result<()> {
136+ let test_case = test_case.into();
137+ let app = Application::new(args, config)?;
138+
139+ test_key_sequence_with_input_text(
140+ Some(app),
141+ test_case.clone(),
142+ &|app| {
143+ let doc = doc!(app.editor);
144+ assert_eq!(&test_case.out_text, doc.text());
145+
146+ let mut selections: Vec<_> = doc.selections().values().cloned().collect();
147+ assert_eq!(1, selections.len());
148+
149+ let sel = selections.pop().unwrap();
150+ assert_eq!(test_case.out_selection, sel);
151+ },
152+ false,
153+ )
154+ .await
155+}
156+
157+pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
158+ test_with_config(Args::default(), Config::default(), test_case).await
159+}
160+
161+pub fn temp_file_with_contents<S: AsRef<str>>(
162+ content: S,
163+) -> anyhow::Result<tempfile::NamedTempFile> {
164+ let mut temp_file = tempfile::NamedTempFile::new()?;
165+
166+ temp_file
167+ .as_file_mut()
168+ .write_all(content.as_ref().as_bytes())?;
169+
170+ temp_file.flush()?;
171+ temp_file.as_file_mut().sync_all()?;
172+ Ok(temp_file)
173+}
174+
175+/// Replaces all LF chars with the system's appropriate line feed
176+/// character, and if one doesn't exist already, appends the system's
177+/// appropriate line ending to the end of a string.
178+pub fn platform_line(input: &str) -> String {
179+ let line_end = helix_core::DEFAULT_LINE_ENDING.as_str();
180+
181+ // we can assume that the source files in this code base will always
182+ // be LF, so indoc strings will always insert LF
183+ let mut output = input.replace('\n', line_end);
184+
185+ if !output.ends_with(line_end) {
186+ output.push_str(line_end);
187+ }
188+
189+ output
190+}
191+
192+/// Creates a new temporary file that is set to read only. Useful for
193+/// testing write failures.
194+pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
195+ let mut file = tempfile::NamedTempFile::new()?;
196+ let metadata = file.as_file().metadata()?;
197+ let mut perms = metadata.permissions();
198+ perms.set_readonly(true);
199+ file.as_file_mut().set_permissions(perms)?;
200+ Ok(file)
201+}
202+
203+/// Creates a new Application with default config that opens the given file
204+/// path
205+pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> {
206+ Application::new(
207+ Args {
208+ files: vec![(path.into(), helix_core::Position::default())],
209+ ..Default::default()
210+ },
211+ Config::default(),
212+ )
213+}
helix-term/tests/test/movement.rsnew file+87
@@ -0,0 +1,87 @@
1+use super::*;
2+
3+#[tokio::test]
4+async fn insert_mode_cursor_position() -> anyhow::Result<()> {
5+ test(TestCase {
6+ in_text: String::new(),
7+ in_selection: Selection::single(0, 0),
8+ in_keys: "i".into(),
9+ out_text: String::new(),
10+ out_selection: Selection::single(0, 0),
11+ })
12+ .await?;
13+
14+ test(("#[\n|]#", "i", "#[|\n]#")).await?;
15+ test(("#[\n|]#", "i<esc>", "#[|\n]#")).await?;
16+ test(("#[\n|]#", "i<esc>i", "#[|\n]#")).await?;
17+
18+ Ok(())
19+}
20+
21+/// Range direction is preserved when escaping insert mode to normal
22+#[tokio::test]
23+async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
24+ test(("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n")).await?;
25+ test((
26+ indoc! {"\
27+ #[f|]#oo
28+ #(b|)#ar"
29+ },
30+ "vll<A-;><esc>",
31+ indoc! {"\
32+ #[|foo]#
33+ #(|bar)#"
34+ },
35+ ))
36+ .await?;
37+
38+ test((
39+ indoc! {"\
40+ #[f|]#oo
41+ #(b|)#ar"
42+ },
43+ "a",
44+ indoc! {"\
45+ #[fo|]#o
46+ #(ba|)#r"
47+ },
48+ ))
49+ .await?;
50+
51+ test((
52+ indoc! {"\
53+ #[f|]#oo
54+ #(b|)#ar"
55+ },
56+ "a<esc>",
57+ indoc! {"\
58+ #[f|]#oo
59+ #(b|)#ar"
60+ },
61+ ))
62+ .await?;
63+
64+ Ok(())
65+}
66+
67+/// Ensure the very initial cursor in an opened file is the width of
68+/// the first grapheme
69+#[tokio::test]
70+async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
71+ let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
72+ let file = helpers::temp_file_with_contents(content)?;
73+ let mut app = helpers::app_with_file(file.path())?;
74+
75+ let (view, doc) = helix_view::current!(app.editor);
76+ let sel = doc.selection(view.id).clone();
77+ assert_eq!(expected_sel, sel);
78+
79+ Ok(())
80+ };
81+
82+ test("foo", Selection::single(0, 1))?;
83+ test("👨‍👩‍👧‍👦 foo", Selection::single(0, 7))?;
84+ test("", Selection::single(0, 0))?;
85+
86+ Ok(())
87+}
helix-term/tests/test/write.rsnew file+169
@@ -0,0 +1,169 @@
1+use std::{
2+ io::{Read, Write},
3+ ops::RangeInclusive,
4+};
5+
6+use helix_core::diagnostic::Severity;
7+use helix_term::application::Application;
8+use helix_view::doc;
9+
10+use super::*;
11+
12+#[tokio::test]
13+async fn test_write() -> anyhow::Result<()> {
14+ let mut file = tempfile::NamedTempFile::new()?;
15+
16+ test_key_sequence(
17+ &mut helpers::app_with_file(file.path())?,
18+ Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
19+ None,
20+ false,
21+ )
22+ .await?;
23+
24+ file.as_file_mut().flush()?;
25+ file.as_file_mut().sync_all()?;
26+
27+ let mut file_content = String::new();
28+ file.as_file_mut().read_to_string(&mut file_content)?;
29+
30+ assert_eq!(
31+ helpers::platform_line("the gostak distims the doshes"),
32+ file_content
33+ );
34+
35+ Ok(())
36+}
37+
38+#[tokio::test]
39+async fn test_write_quit() -> anyhow::Result<()> {
40+ let mut file = tempfile::NamedTempFile::new()?;
41+
42+ test_key_sequence(
43+ &mut helpers::app_with_file(file.path())?,
44+ Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
45+ None,
46+ true,
47+ )
48+ .await?;
49+
50+ file.as_file_mut().flush()?;
51+ file.as_file_mut().sync_all()?;
52+
53+ let mut file_content = String::new();
54+ file.as_file_mut().read_to_string(&mut file_content)?;
55+
56+ assert_eq!(
57+ helpers::platform_line("the gostak distims the doshes"),
58+ file_content
59+ );
60+
61+ Ok(())
62+}
63+
64+#[tokio::test]
65+#[ignore]
66+async fn test_write_concurrent() -> anyhow::Result<()> {
67+ let mut file = tempfile::NamedTempFile::new()?;
68+ let mut command = String::new();
69+ const RANGE: RangeInclusive<i32> = 1..=5000;
70+
71+ for i in RANGE {
72+ let cmd = format!("%c{}<esc>:w<ret>", i);
73+ command.push_str(&cmd);
74+ }
75+
76+ test_key_sequence(
77+ &mut helpers::app_with_file(file.path())?,
78+ Some(&command),
79+ None,
80+ false,
81+ )
82+ .await?;
83+
84+ file.as_file_mut().flush()?;
85+ file.as_file_mut().sync_all()?;
86+
87+ let mut file_content = String::new();
88+ file.as_file_mut().read_to_string(&mut file_content)?;
89+ assert_eq!(RANGE.end().to_string(), file_content);
90+
91+ Ok(())
92+}
93+
94+#[tokio::test]
95+#[ignore]
96+async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
97+ let file = helpers::new_readonly_tempfile()?;
98+
99+ test_key_sequences(
100+ &mut helpers::app_with_file(file.path())?,
101+ vec![
102+ (
103+ None,
104+ Some(&|app| {
105+ let doc = doc!(app.editor);
106+ assert!(!doc.is_modified());
107+ }),
108+ ),
109+ (
110+ Some("ihello<esc>"),
111+ Some(&|app| {
112+ let doc = doc!(app.editor);
113+ assert!(doc.is_modified());
114+ }),
115+ ),
116+ (
117+ Some(":w<ret>"),
118+ Some(&|app| {
119+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
120+
121+ let doc = doc!(app.editor);
122+ assert!(doc.is_modified());
123+ }),
124+ ),
125+ ],
126+ false,
127+ )
128+ .await?;
129+
130+ Ok(())
131+}
132+
133+#[tokio::test]
134+#[ignore]
135+async fn test_write_fail_new_path() -> anyhow::Result<()> {
136+ let file = helpers::new_readonly_tempfile()?;
137+
138+ test_key_sequences(
139+ &mut Application::new(Args::default(), Config::default())?,
140+ vec![
141+ (
142+ None,
143+ Some(&|app| {
144+ let doc = doc!(app.editor);
145+ assert_ne!(
146+ Some(&Severity::Error),
147+ app.editor.get_status().map(|status| status.1)
148+ );
149+ assert_eq!(None, doc.path());
150+ }),
151+ ),
152+ (
153+ Some(&format!(":w {}<ret>", file.path().to_string_lossy())),
154+ Some(&|app| {
155+ let doc = doc!(app.editor);
156+ assert_eq!(
157+ Some(&Severity::Error),
158+ app.editor.get_status().map(|status| status.1)
159+ );
160+ assert_eq!(None, doc.path());
161+ }),
162+ ),
163+ ],
164+ false,
165+ )
166+ .await?;
167+
168+ Ok(())
169+}
helix-view/src/document.rs+33−1
@@ -1,5 +1,6 @@
1use anyhow::{anyhow, bail, Context, Error};
1use anyhow::{anyhow, bail, Context, Error};
2use helix_core::auto_pairs::AutoPairs;
2use helix_core::auto_pairs::AutoPairs;
3+use helix_core::Range;
3use serde::de::{self, Deserialize, Deserializer};
4use serde::de::{self, Deserialize, Deserializer};
4use serde::Serialize;
5use serde::Serialize;
5use std::cell::Cell;
6use std::cell::Cell;
@@ -83,7 +84,7 @@ impl Serialize for Mode {
83pub struct Document {
84pub struct Document {
84 pub(crate) id: DocumentId,
85 pub(crate) id: DocumentId,
85 text: Rope,
86 text: Rope,
86− pub(crate) selections: HashMap<ViewId, Selection>,
87+ selections: HashMap<ViewId, Selection>,
87
88
88 path: Option<PathBuf>,
89 path: Option<PathBuf>,
89 encoding: &'static encoding::Encoding,
90 encoding: &'static encoding::Encoding,
@@ -637,6 +638,37 @@ impl Document {
637 .insert(view_id, selection.ensure_invariants(self.text().slice(..)));
638 .insert(view_id, selection.ensure_invariants(self.text().slice(..)));
638 }
639 }
639
640
641+ /// Find the origin selection of the text in a document, i.e. where
642+ /// a single cursor would go if it were on the first grapheme. If
643+ /// the text is empty, returns (0, 0).
644+ pub fn origin(&self) -> Range {
645+ if self.text().len_chars() == 0 {
646+ return Range::new(0, 0);
647+ }
648+
649+ Range::new(0, 1).grapheme_aligned(self.text().slice(..))
650+ }
651+
652+ /// Reset the view's selection on this document to the
653+ /// [origin](Document::origin) cursor.
654+ pub fn reset_selection(&mut self, view_id: ViewId) {
655+ let origin = self.origin();
656+ self.set_selection(view_id, Selection::single(origin.anchor, origin.head));
657+ }
658+
659+ /// Initializes a new selection for the given view if it does not
660+ /// already have one.
661+ pub fn ensure_view_init(&mut self, view_id: ViewId) {
662+ if self.selections.get(&view_id).is_none() {
663+ self.reset_selection(view_id);
664+ }
665+ }
666+
667+ /// Remove a view's selection from this document.
668+ pub fn remove_view(&mut self, view_id: ViewId) {
669+ self.selections.remove(&view_id);
670+ }
671+
640 /// Apply a [`Transaction`] to the [`Document`] to change its text.
672 /// Apply a [`Transaction`] to the [`Document`] to change its text.
641 fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
673 fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
642 let old_doc = self.text().clone();
674 let old_doc = self.text().clone();
helix-view/src/editor.rs+23−18
@@ -32,12 +32,12 @@ use anyhow::{bail, Error};
32
32
33pub use helix_core::diagnostic::Severity;
33pub use helix_core::diagnostic::Severity;
34pub use helix_core::register::Registers;
34pub use helix_core::register::Registers;
35+use helix_core::Position;
35use helix_core::{
36use helix_core::{
36 auto_pairs::AutoPairs,
37 auto_pairs::AutoPairs,
37 syntax::{self, AutoPairConfig},
38 syntax::{self, AutoPairConfig},
38 Change,
39 Change,
39};
40};
40−use helix_core::{Position, Selection};
41use helix_dap as dap;
41use helix_dap as dap;
42
42
43use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
43use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
@@ -590,6 +590,20 @@ impl Editor {
590 self.status_msg = Some((error.into(), Severity::Error));
590 self.status_msg = Some((error.into(), Severity::Error));
591 }
591 }
592
592
593+ #[inline]
594+ pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
595+ self.status_msg.as_ref().map(|(status, sev)| (status, sev))
596+ }
597+
598+ /// Returns true if the current status is an error
599+ #[inline]
600+ pub fn is_err(&self) -> bool {
601+ self.status_msg
602+ .as_ref()
603+ .map(|(_, sev)| *sev == Severity::Error)
604+ .unwrap_or(false)
605+ }
606+
593 pub fn set_theme(&mut self, theme: Theme) {
607 pub fn set_theme(&mut self, theme: Theme) {
594 // `ui.selection` is the only scope required to be able to render a theme.
608 // `ui.selection` is the only scope required to be able to render a theme.
595 if theme.find_scope_index("ui.selection").is_none() {
609 if theme.find_scope_index("ui.selection").is_none() {
@@ -664,11 +678,8 @@ impl Editor {
664 view.offset = Position::default();
678 view.offset = Position::default();
665
679
666 let doc = self.documents.get_mut(&doc_id).unwrap();
680 let doc = self.documents.get_mut(&doc_id).unwrap();
681+ doc.ensure_view_init(view.id);
667
682
668− // initialize selection for view
669− doc.selections
670− .entry(view.id)
671− .or_insert_with(|| Selection::point(0));
672 // TODO: reuse align_view
683 // TODO: reuse align_view
673 let pos = doc
684 let pos = doc
674 .selection(view.id)
685 .selection(view.id)
@@ -738,9 +749,7 @@ impl Editor {
738 Action::Load => {
749 Action::Load => {
739 let view_id = view!(self).id;
750 let view_id = view!(self).id;
740 let doc = self.documents.get_mut(&id).unwrap();
751 let doc = self.documents.get_mut(&id).unwrap();
741− if doc.selections().is_empty() {
752+ doc.ensure_view_init(view_id);
742− doc.set_selection(view_id, Selection::point(0));
743− }
744 return;
753 return;
745 }
754 }
746 Action::HorizontalSplit | Action::VerticalSplit => {
755 Action::HorizontalSplit | Action::VerticalSplit => {
@@ -755,7 +764,7 @@ impl Editor {
755 );
764 );
756 // initialize selection for view
765 // initialize selection for view
757 let doc = self.documents.get_mut(&id).unwrap();
766 let doc = self.documents.get_mut(&id).unwrap();
758− doc.set_selection(view_id, Selection::point(0));
767+ doc.ensure_view_init(view_id);
759 }
768 }
760 }
769 }
761
770
@@ -788,8 +797,9 @@ impl Editor {
788 Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
797 Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
789 }
798 }
790
799
800+ // ??? possible use for integration tests
791− pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
801+ pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
792− let path = helix_core::path::get_canonicalized_path(&path)?;
802+ let path = helix_core::path::get_canonicalized_path(path)?;
793 let id = self.document_by_path(&path).map(|doc| doc.id);
803 let id = self.document_by_path(&path).map(|doc| doc.id);
794
804
795 let id = if let Some(id) = id {
805 let id = if let Some(id) = id {
@@ -809,12 +819,7 @@ impl Editor {
809 pub fn close(&mut self, id: ViewId) {
819 pub fn close(&mut self, id: ViewId) {
810 let view = self.tree.get(self.tree.focus);
820 let view = self.tree.get(self.tree.focus);
811 // remove selection
821 // remove selection
812− self.documents
813− .get_mut(&view.doc)
822+ self.documents.get_mut(&view.doc).unwrap().remove_view(id);
814− .unwrap()
815− .selections
816− .remove(&id);
817−
818 self.tree.remove(id);
823 self.tree.remove(id);
819 self._refresh();
824 self._refresh();
820 }
825 }
@@ -889,7 +894,7 @@ impl Editor {
889 let view = View::new(doc_id, self.config().gutters.clone());
894 let view = View::new(doc_id, self.config().gutters.clone());
890 let view_id = self.tree.insert(view);
895 let view_id = self.tree.insert(view);
891 let doc = self.documents.get_mut(&doc_id).unwrap();
896 let doc = self.documents.get_mut(&doc_id).unwrap();
892− doc.set_selection(view_id, Selection::point(0));
897+ doc.ensure_view_init(view_id);
893 }
898 }
894
899
895 self._refresh();
900 self._refresh();
helix-view/src/handlers/dap.rs+1−1
@@ -62,7 +62,7 @@ pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) {
62 return;
62 return;
63 };
63 };
64
64
65− if let Err(e) = editor.open(path, Action::Replace) {
65+ if let Err(e) = editor.open(&path, Action::Replace) {
66 editor.set_error(format!("Unable to jump to stack frame: {}", e));
66 editor.set_error(format!("Unable to jump to stack frame: {}", e));
67 return;
67 return;
68 }
68 }