diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml deleted file mode 100644 index 223f8450..00000000 --- a/.github/workflows/gh-pages.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Github Pages - -on: - push: - branches: - - master - tags: - - '*' - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 - with: - mdbook-version: 'latest' - # mdbook-version: '0.4.8' - - - run: mdbook build book - - - name: Set output directory - run: | - OUTDIR=$(basename ${{ github.ref }}) - echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./book/book - destination_dir: ./${{ env.OUTDIR }} - - - name: Deploy stable - uses: peaceiris/actions-gh-pages@v3 - if: startswith(github.ref, 'refs/tags/') - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./book/book diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9518a537..547d2480 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,8 +116,10 @@ jobs: - name: Install ${{ matrix.rust }} toolchain uses: dtolnay/rust-toolchain@master with: + profile: minimal toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} + override: true # Install a pre-release version of Cross # TODO: We need to pre-install Cross because we need cross-rs/cross#591 to @@ -137,8 +139,12 @@ jobs: echo "target flag is: ${{ env.TARGET_FLAGS }}" - name: Run cargo test + uses: actions-rs/cargo@v1 if: "!matrix.skip_tests" - run: ${{ env.CARGO }} test --release --locked --target ${{ matrix.target }} --workspace + with: + use-cross: ${{ matrix.cross }} + command: test + args: --release --locked --target ${{ matrix.target }} --workspace - name: Set profile.release.strip = true shell: bash diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/helix.iml b/.idea/helix.iml new file mode 100644 index 00000000..bc2cd874 --- /dev/null +++ b/.idea/helix.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..03d9549e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..6c65ec58 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 69ba8444..d834f3de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,20 +84,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", -] - -[[package]] -name = "bstr" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" +checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" dependencies = [ "memchr", "once_cell", @@ -250,15 +239,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossterm" version = "0.25.0" @@ -287,9 +267,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" dependencies = [ "cc", "cxxbridge-flags", @@ -299,9 +279,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" dependencies = [ "cc", "codespan-reporting", @@ -314,15 +294,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" dependencies = [ "proc-macro2", "quote", @@ -339,7 +319,7 @@ dependencies = [ "hashbrown 0.12.3", "lock_api", "once_cell", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.6", ] [[package]] @@ -448,9 +428,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if", "libc", @@ -545,7 +525,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9e5fd7bc63ad527d64584f8d01f99b89c051f5fbb8144b58ae5f812775065cf" dependencies = [ - "bstr 1.0.1", + "bstr", "btoi", "git-date", "itoa", @@ -555,11 +535,11 @@ dependencies = [ [[package]] name = "git-attributes" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8013dfce47c1e29236d732308933e2c77af5355ec5105755d26faf7764d3f7b" +checksum = "b2c9687a890892650e8574e123b4b633d277b99953cb877dc02aba852a0139fa" dependencies = [ - "bstr 1.0.1", + "bstr", "compact_str", "git-features", "git-glob", @@ -589,20 +569,20 @@ dependencies = [ [[package]] name = "git-command" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "215145cc1686a45bc6f9872b153a0d3f3c40a1b94173a928325e1b53dfa5e2af" +checksum = "5a19fe1efc0b4969b2b2a14621f6cf6a007cf6cbabcf344e078271b65d1f7cef" dependencies = [ - "bstr 1.0.1", + "bstr", ] [[package]] name = "git-config" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9da662fd64ac69772158dcf04777da6266f0f36bc9a310b3eb2d805bb696315" +checksum = "500cc0517781f9f573c4dc26feb3ae0cdc28ae7160a81ef104590943984f6a8e" dependencies = [ - "bstr 1.0.1", + "bstr", "git-config-value", "git-features", "git-glob", @@ -624,7 +604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989a90c1c630513a153c685b4249b96fdf938afc75bf7ef2ae1ccbd3d799f5db" dependencies = [ "bitflags", - "bstr 1.0.1", + "bstr", "git-path", "libc", "thiserror", @@ -636,7 +616,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97cd6bbe001afd6356b35ef13f2a6b0f0abc0133d1b2ecaec1033bdd769616d6" dependencies = [ - "bstr 1.0.1", + "bstr", "git-command", "git-config-value", "git-path", @@ -648,11 +628,11 @@ dependencies = [ [[package]] name = "git-date" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "412c9b89026505bd24d5f8acafa578de6eea3b271ece307a73b8e646e671302a" +checksum = "3777ed3a92334193bc5032d468dee2ddb7d1101263e58e0d2edcc308c06948b5" dependencies = [ - "bstr 1.0.1", + "bstr", "itoa", "thiserror", "time", @@ -672,11 +652,11 @@ dependencies = [ [[package]] name = "git-discover" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9e26e0bc434643228cd418185bd28ca5c7cf831bde1da434807391c27ac40e" +checksum = "a2738a9941f1411cff31e6ea4399a6c7304cc3ea34fb8c1c6f1aef1f667d46cc" dependencies = [ - "bstr 1.0.1", + "bstr", "git-hash", "git-path", "git-ref", @@ -686,9 +666,9 @@ dependencies = [ [[package]] name = "git-features" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff74064fa007c5beefa89a64bb72834f32b3c497750a56c79c6802bbdb311f9" +checksum = "0019327672cb759f851d1b18fdcc36bb797dc62b925cb93c8c881b54735eb2c2" dependencies = [ "crc32fast", "flate2", @@ -703,12 +683,12 @@ dependencies = [ [[package]] name = "git-glob" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3908404c9b76ac7b3f636a104142378d3eaa78623cbc6eb7c7f0651979d48e8a" +checksum = "aa73cf9c9c1a66e28de1cf250fc1ebe323e7c7c59768c1a2331e3b3308e783a3" dependencies = [ "bitflags", - "bstr 1.0.1", + "bstr", ] [[package]] @@ -728,18 +708,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c52b625ad8cc360a0b7f426266f21fb07bd49b8f4ccf1b3ca7bc89424db1dec4" dependencies = [ "git-hash", - "hashbrown 0.13.1", + "hashbrown 0.13.2", ] [[package]] name = "git-index" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485da97dd4f69c7d9a8dc238cd6f4a726387ffc34573489e8e0d2bee266e3454" +checksum = "b82fd3d70ed6fbceb7573f145fbf79371e4d0c8dbdf7ad46f3a03328239ddda7" dependencies = [ "atoi", "bitflags", - "bstr 1.0.1", + "bstr", "filetime", "git-bitmap", "git-features", @@ -755,9 +735,9 @@ dependencies = [ [[package]] name = "git-lock" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e4f05b8a68c3a5dd83a6651c76be384e910fe283072184fdab9d77f87ccec2" +checksum = "e7cf6a3c9d1a9932bb9bcb7e0044e2e429f9d94711969a7d2a09e34ae21f6437" dependencies = [ "fastrand", "git-tempfile", @@ -766,11 +746,11 @@ dependencies = [ [[package]] name = "git-mailmap" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0316b4346f3e162ade368209efb8a609b587793c74aa3b8de0ec01a4f3580120" +checksum = "1957f2f550e345f70cb0615d390fb0446d41eeb5bc87824b7bae31efd8cfc2da" dependencies = [ - "bstr 1.0.1", + "bstr", "git-actor", "quick-error", ] @@ -781,7 +761,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8563e2d6f524d7053f3106714f99ecdc3adbba2cb7108c09d71a02579f2e19" dependencies = [ - "bstr 1.0.1", + "bstr", "btoi", "git-actor", "git-features", @@ -796,9 +776,9 @@ dependencies = [ [[package]] name = "git-odb" -version = "0.40.0" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616115a0e3daff6e08842758d24547b37a6eb6d0e2eedd95a740c3aaa2750333" +checksum = "b43514e1d1062613352a10f96d48e69967f4c420776596e1af9a6f368df2eea2" dependencies = [ "arc-swap", "git-features", @@ -814,9 +794,9 @@ dependencies = [ [[package]] name = "git-pack" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd16b88f4b66041f41ca510c28bd81c4ee7363c5a544b3d62b4170432965871" +checksum = "9e124d13e4e4b53ca7544e9786f30dafe2e76b4a75ba62b2156ee0656b356c71" dependencies = [ "bytesize", "clru", @@ -842,15 +822,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e40e68481a06da243d3f4dfd86a4be39c24eefb535017a862e845140dcdb878a" dependencies = [ - "bstr 1.0.1", + "bstr", "thiserror", ] [[package]] name = "git-prompt" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3612a486e507dd431ef0f7108eeaafc8fd1ed7bd0f205a88554f6f91fe5dccbf" +checksum = "ad3f84ec28896f6a4b3f3174a1125117ac91788b1c64d96f25eabcd8d01cc7e3" dependencies = [ "git-command", "git-config-value", @@ -865,16 +845,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd11f4e7f251ab297545faa4c5a4517f4985a43b9c16bf96fa49107f58e837f" dependencies = [ - "bstr 1.0.1", + "bstr", "btoi", "quick-error", ] [[package]] name = "git-ref" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6767925a6fc4af5c5a81e348d1d851c1b3ab2b512bd7f562ac11be37c14468" +checksum = "2a2c29bab109acaf626d49a54f1f85ab7f0911268fbf62c2b39680ef4ef19069" dependencies = [ "git-actor", "git-features", @@ -891,11 +871,11 @@ dependencies = [ [[package]] name = "git-refspec" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf310ed5f2829ac0af96e7d4aebd4ae4b89f0718a7ae3666d09b02b2c5a1dfd" +checksum = "419fba469ca7dca4746de8b2be6a21990b276f3974acaa94314f39d4c2bbfc0a" dependencies = [ - "bstr 1.0.1", + "bstr", "git-hash", "git-revision", "git-validate", @@ -905,9 +885,9 @@ dependencies = [ [[package]] name = "git-repository" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993277960cb7e2d3991a11c1ec6951c1d142de052c26a18d2db64304e52d3741" +checksum = "1a958d3fa83660d15c6535765477a8895156bdd6a922dbd0fc445afa42f4b534" dependencies = [ "git-actor", "git-attributes", @@ -948,11 +928,11 @@ dependencies = [ [[package]] name = "git-revision" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9a6bd28c9d1676bb96f428cd09614ae18a0087d7cea1cebfd177e25f99b2af" +checksum = "bfc3f7c901777f8318f059dbdf73dbda05acdb36c631fe12465bd955e230a205" dependencies = [ - "bstr 1.0.1", + "bstr", "git-date", "git-hash", "git-hashtable", @@ -962,9 +942,9 @@ dependencies = [ [[package]] name = "git-sec" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1802e8252fa223b0ad89a393aed461132174ced1e6842a41f56dc92a3fc14f" +checksum = "6696a816445a51f76995d579a3122f98247377cc45cd681764f740f3a2666004" dependencies = [ "bitflags", "dirs", @@ -975,9 +955,9 @@ dependencies = [ [[package]] name = "git-tempfile" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6bb4dee86c8cae5a078cfaac3b004ef99c31548ed86218f23a7ff9b4b74f3be" +checksum = "2d851911a2b043dc1ab6cd5432ce7a3ee3a2fd614ed87428cec1b15f5abb7e0c" dependencies = [ "dashmap", "libc", @@ -1001,11 +981,11 @@ dependencies = [ [[package]] name = "git-url" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85af407ed0dbb8d8da2a7241827d2fd5681186d9dab3570fc8dd8d6152ec48f" +checksum = "cc9a3df0498c511cf34739eab2692352939b54075c2fc96e8f688d402f3f1250" dependencies = [ - "bstr 1.0.1", + "bstr", "git-features", "git-path", "home", @@ -1019,17 +999,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0431cf9352c596dc7c8ec9066ee551ce54e63c86c3c767e5baf763f6019ff3c2" dependencies = [ - "bstr 1.0.1", + "bstr", "thiserror", ] [[package]] name = "git-worktree" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3bc63878f134e08ed52dba5d82422798c01a3f2e48c38ae9a2f7ff9194f362" +checksum = "0c28b292694c98bba8225c39d4e86605843882ba7117ca98491841761e710547" dependencies = [ - "bstr 1.0.1", + "bstr", "git-attributes", "git-features", "git-glob", @@ -1043,12 +1023,12 @@ dependencies = [ [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick", - "bstr 0.2.17", + "bstr", "fnv", "log", "regex", @@ -1056,21 +1036,21 @@ dependencies = [ [[package]] name = "grep-matcher" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" +checksum = "3902ca28f26945fe35cad349d776f163981d777fee382ccd6ef451126f51b319" dependencies = [ "memchr", ] [[package]] name = "grep-regex" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e" +checksum = "997598b41d53a37a2e3fc5300d5c11d825368c054420a9c65125b8fe1078463f" dependencies = [ "aho-corasick", - "bstr 0.2.17", + "bstr", "grep-matcher", "log", "regex", @@ -1080,11 +1060,11 @@ dependencies = [ [[package]] name = "grep-searcher" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc" +checksum = "5601c4b9f480f0c9ebb40b1f6cbf447b8a50c5369223937a6c5214368c58779f" dependencies = [ - "bstr 0.2.17", + "bstr", "bytecount", "encoding_rs", "encoding_rs_io", @@ -1104,9 +1084,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash 0.8.2", ] @@ -1121,7 +1101,7 @@ dependencies = [ "chrono", "encoding_rs", "etcetera", - "hashbrown 0.13.1", + "hashbrown 0.13.2", "helix-loader", "imara-diff", "log", @@ -1288,9 +1268,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] @@ -1352,11 +1332,10 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3" dependencies = [ - "crossbeam-utils", "globset", "lazy_static", "log", @@ -1405,9 +1384,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "js-sys" @@ -1426,9 +1405,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libloading" @@ -1442,9 +1421,9 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] @@ -1537,9 +1516,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", @@ -1566,9 +1545,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ "hermit-abi", "libc", @@ -1607,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.6", ] [[package]] @@ -1626,9 +1605,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" dependencies = [ "cfg-if", "libc", @@ -1657,9 +1636,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] @@ -1704,9 +1683,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -1793,15 +1772,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "same-file" @@ -1820,9 +1799,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" [[package]] name = "serde" @@ -1857,9 +1836,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" dependencies = [ "proc-macro2", "quote", @@ -1979,15 +1958,15 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "str_indices" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0" +checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" [[package]] name = "syn" -version = "1.0.104" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -2141,9 +2120,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -2209,9 +2188,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-linebreak" @@ -2377,17 +2356,17 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.40.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e30acc718a52fb130fec72b1cb5f55ffeeec9253e1b785e94db222178a6acaa1" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm 0.40.0", - "windows_aarch64_msvc 0.40.0", - "windows_i686_gnu 0.40.0", - "windows_i686_msvc 0.40.0", - "windows_x86_64_gnu 0.40.0", - "windows_x86_64_gnullvm 0.40.0", - "windows_x86_64_msvc 0.40.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2396,98 +2375,56 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3caa4a1a16561b714323ca6b0817403738583033a6a92e04c5d10d4ba37ca10" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328973c62dfcc50fb1aaa8e7100676e0b642fe56bac6bafff3327902db843ab4" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" - -[[package]] -name = "windows_i686_gnu" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa5b09fad70f0df85dea2ac2a525537e415e2bf63ee31cf9b8e263645ee9f3c1" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" - -[[package]] -name = "windows_i686_msvc" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1ad4031c1a98491fa195d8d43d7489cb749f135f2e5c4eed58da094bd0d876" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520ff37edd72da8064b49d2281182898e17f0688ae9f4070bca27e4b5c162ac7" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046e5b82215102c44fd75f488f1b9158973d02aa34d06ed85c23d6f5520a2853" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0c9c6df55dd1bfa76e131cef44bdd8ec9c819ef3611f04dfe453fd5bfeda28" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "xtask" diff --git a/README.md b/README.md index 60125e11..5b7b54f4 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,6 @@ -
- -

- - - - Helix - -

+# Helix [![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions) -[![GitHub Release](https://img.shields.io/github/v/release/helix-editor/helix)](https://github.com/helix-editor/helix/releases/latest) -[![Documentation](https://shields.io/badge/-documentation-452859)](https://docs.helix-editor.com/) -[![GitHub contributors](https://img.shields.io/github/contributors/helix-editor/helix)](https://github.com/helix-editor/helix/graphs/contributors) -[![Matrix Space](https://img.shields.io/matrix/helix-community:matrix.org)](https://matrix.to/#/#helix-community:matrix.org) - -
![Screenshot](./screenshot.png) @@ -139,7 +125,3 @@ Contributing guidelines can be found [here](./docs/CONTRIBUTING.md). Your question might already be answered on the [FAQ](https://github.com/helix-editor/helix/wiki/FAQ). Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet). - -# Credits - -Thanks to [@JakeHL](https://github.com/JakeHL) for designing the logo! diff --git a/book/src/configuration.md b/book/src/configuration.md index a35482e6..af80b177 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -52,6 +52,7 @@ on unix operating systems. | `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | +| `completion-trigger-chars` | The chars that trigger completion (additional to all word chars) | `['.', ':']` | | `auto-info` | Whether to display infoboxes | `true` | | `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | @@ -172,6 +173,8 @@ auto-pairs = false # defaults to `true` The default pairs are (){}[]''""``, but these can be customized by setting `auto-pairs` to a TOML table: +Example + ```toml [editor.auto-pairs] '(' = ')' @@ -237,22 +240,85 @@ tab = "→" newline = "⏎" tabpad = "·" # Tabs will look like "→···" (depending on tab width) ``` +<<<<<<< HEAD + +### `[editor.explorer]` Section +Sets explorer side width and style. + + | Key | Description | Default | + | --- | ----------- | ------- | + | `column-width` | explorer side width | 30 | + | `style` | explorer item style, tree or list | tree | + | `position` | explorer widget position, embed or overlay | overlay | +||||||| 43027d91 +======= ### `[editor.indent-guides]` Section Options for rendering vertical indent guides. +<<<<<<< HEAD +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `true` | +| `character` | Literal character to use for rendering the indent guide | `│` | +| `rainbow` | Whether or not the indent guides shall have changing colors. | `false` | +| `skip-levels` | Number of indent levels to skip | `0` | +||||||| merged common ancestors +<<<<<<<<< Temporary merge branch 1 +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | +| `rainbow` | Whether or not the indent guides shall have changing colors. | `false` | +||||||||| 60aa7d36 +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | +========= | Key | Description | Default | | --- | --- | --- | | `render` | Whether to render indent guides. | `false` | | `character` | Literal character to use for rendering the indent guide | `│` | | `skip-levels` | Number of indent levels to skip | `0` | +>>>>>>>>> Temporary merge branch 2 +======= +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | +| `rainbow` | Whether or not the indent guides shall have changing colors. It can be `none`, `dim` or `normal`| `none` | +| `skip-levels` | Number of indent levels to skip | `0` | +>>>>>>> colored-indent-guides Example: ```toml [editor.indent-guides] render = true +character = "╎" +<<<<<<< HEAD +rainbow = true +||||||| merged common ancestors +rainbow = true +||||||||| 60aa7d36 +character = "╎" +========= character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽" +======= +rainbow = "normal" +character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽" +>>>>>>> colored-indent-guides skip-levels = 1 ``` + +### `[editor.explorer]` Section +Sets explorer side width and style. + + | Key | Description | Default | + | --- | ----------- | ------- | + | `column-width` | explorer side width | 30 | + | `style` | explorer item style, tree or list | tree | + | `position` | explorer widget position, embed or overlay | overlay | +>>>>>>> 0e04c4c93caadb704c11a72bcf626b1f10ff2d98 diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 66e6ac03..434a343b 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -73,3 +73,4 @@ | `:pipe` | Pipe each selection to the shell command. | | `:pipe-to` | Pipe each selection to the shell command, ignoring output. | | `:run-shell-command`, `:sh` | Run a shell command | +| `:lsp-restart` | Restarts the LSP server of the current buffer | diff --git a/book/src/keymap.md b/book/src/keymap.md index b38a1f44..1eb48a6f 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -287,6 +287,8 @@ This layer is a kludge of mappings, mostly pickers. | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | | `/` | Global search in workspace folder | `global_search` | | `?` | Open command palette | `command_palette` | +| `e` | Open or focus explorer | `toggle_or_focus_explorer` | +| `E` | open explorer recursion | `open_explorer_recursion` | > TIP: Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. @@ -437,3 +439,35 @@ Keys to use within prompt, Remapping currently not supported. | `Tab` | Select next completion item | | `BackTab` | Select previous completion item | | `Enter` | Open selected | + +# File explorer +Keys to use within explorer, Remapping currently not supported. + +| Key | Description | +| ----- | ------------- | +| `Escape` | Back to editor | +| `Ctrl-c` | Close explorer | +| `Enter` | Open file or toggle dir selected | +| `b` | Back to current root's parent | +| `f` | Filter items | +| `z` | Fold currrent level | +| `k`, `Shift-Tab`, `Up` | select previous item | +| `j`, `Tab`, `Down` | select next item | +| `h` | Scroll left | +| `l` | Scroll right | +| `G` | Move to last item | +| `Ctrl-d` | Move down half page | +| `Ctrl-u` | Move up half page | +| `Shift-d` | Move down a page | +| `Shift-u` | Move up a page | +| `/` | Search item | +| `?` | Search item reverse | +| `n` | Repeat last search | +| `Shift-n` | Repeat last search reverse | +| `gg` | Move to first item | +| `ge` | Move to last item | +| `gc` | Make current dir as root dir | +| `mf` | Create new file under current item's parent | +| `md` | Create new dir under current item's parent | +| `rf` | Remove file selected | +| `rd` | Remove dir selected | diff --git a/book/src/themes.md b/book/src/themes.md index 015ec59b..37ee0924 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -107,6 +107,35 @@ Some styles might not be supported by your terminal emulator. | `double_line` | +<<<<<<< HEAD +<<<<<<< HEAD +### Rainbow + +The `rainbow` key is used for rainbow highlight for matching brackets. +The key is a list of styles. + +```toml +rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }] +``` + +Colors from the palette and modifiers may be used. + +||||||| 60aa7d36 +======= +||||||| merged common ancestors +======= +### Rainbow + +The `rainbow` key is used for rainbow highlight for matching brackets. +The key is a list of styles. + +```toml +rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }] +``` + +Colors from the palette and modifiers may be used. + +>>>>>>> colored-indent-guides ### Inheritance Extend upon other themes by setting the `inherits` property to an existing theme. @@ -122,6 +151,22 @@ inherits = "boo_berry" berry = "#2A2A4D" ``` +<<<<<<< HEAD +>>>>>>> seperate_code_action +||||||| merged common ancestors +### Rainbow + +The `rainbow` key is used for rainbow highlight for matching brackets. +The key is a list of styles. + +```toml +rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold"] }] +``` + +Colors from the palette and modifiers may be used. + +======= +>>>>>>> colored-indent-guides ### Scopes The following is a list of scopes available to use for styling. diff --git a/contrib/themes b/contrib/themes deleted file mode 120000 index d09bf827..00000000 --- a/contrib/themes +++ /dev/null @@ -1 +0,0 @@ -../runtime/themes \ No newline at end of file diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ca6cd51e..4404ea96 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -18,34 +18,34 @@ integration = [] helix-loader = { version = "0.6", path = "../helix-loader" } ropey = { version = "1.5.1", default-features = false, features = ["simd"] } -smallvec = "1.10" +smallvec = "1.10.0" smartstring = "1.0.1" -unicode-segmentation = "1.10" -unicode-width = "0.1" -unicode-general-category = "0.6" +unicode-segmentation = "1.10.0" +unicode-width = "0.1.10" +unicode-general-category = "0.6.0" # slab = "0.4.2" -slotmap = "1.0" -tree-sitter = "0.20" -once_cell = "1.17" -arc-swap = "1" -regex = "1" -bitflags = "1.3" +slotmap = "1.0.6" +tree-sitter = "0.20.9" +once_cell = "1.17.0" +arc-swap = "1.6.0" +regex = "1.7.1" +bitflags = "1.3.2" ahash = "0.8.2" -hashbrown = { version = "0.13.1", features = ["raw"] } +hashbrown = { version = "0.13.2", features = ["raw"] } -log = "0.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.5" +log = "0.4.17" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +toml = "0.5.10" -imara-diff = "0.1.0" +imara-diff = "0.1.5" -encoding_rs = "0.8" +encoding_rs = "0.8.31" -chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } +chrono = { version = "0.4.23", default-features = false, features = ["alloc", "std"] } -etcetera = "0.4" +etcetera = "0.4.0" textwrap = "0.16.0" [dev-dependencies] -quickcheck = { version = "1", default-features = false } +quickcheck = { version = "1.0.3", default-features = false } diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index 675f5750..98b9dac4 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -190,6 +190,8 @@ pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize { if char_idx == slice.len_chars() { char_idx + } else if char_idx > slice.len_chars() { + slice.len_chars() } else { prev_grapheme_boundary(slice, char_idx + 1) } diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 482fd6d9..3f695085 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -394,10 +394,12 @@ impl ChangeSet { } if pos > old_pos { - panic!( + log::error!( "Position {} is out of range for changeset len {}!", - pos, old_pos - ) + pos, + old_pos + ); + return old_pos; } new_pos } diff --git a/helix-core/tests/data/indent/indent.rs b/helix-core/tests/data/indent/indent.rs deleted file mode 120000 index 2ac16cf9..00000000 --- a/helix-core/tests/data/indent/indent.rs +++ /dev/null @@ -1 +0,0 @@ -../../../src/indent.rs \ No newline at end of file diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index 95a05905..aef3af20 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -13,13 +13,13 @@ homepage = "https://helix-editor.com" [dependencies] helix-core = { version = "0.6", path = "../helix-core" } -anyhow = "1.0" -log = "0.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "1.0" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } -which = "4.2" +anyhow = "1.0.68" +log = "0.4.17" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +thiserror = "1.0.38" +tokio = { version = "1.24.1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } +which = "4.3.0" [dev-dependencies] -fern = "0.6" +fern = "0.6.1" diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index a3d14584..299ae2fc 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -14,19 +14,19 @@ name = "hx-loader" path = "src/main.rs" [dependencies] -anyhow = "1" -serde = { version = "1.0", features = ["derive"] } -toml = "0.5" -etcetera = "0.4" -tree-sitter = "0.20" -once_cell = "1.17" -log = "0.4" +anyhow = "1.0.68" +serde = { version = "1.0.152", features = ["derive"] } +toml = "0.5.10" +etcetera = "0.4.0" +tree-sitter = "0.20.9" +once_cell = "1.17.0" +log = "0.4.17" # TODO: these two should be on !wasm32 only # cloning/compiling tree-sitter grammars -cc = { version = "1" } -threadpool = { version = "1.0" } +cc = "1.0.78" +threadpool = "1.8.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -libloading = "0.7" +libloading = "0.7.4" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index d31731d2..6ecfa46c 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -15,14 +15,14 @@ homepage = "https://helix-editor.com" helix-core = { version = "0.6", path = "../helix-core" } helix-loader = { version = "0.6", path = "../helix-loader" } -anyhow = "1.0" -futures-executor = "0.3" -futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } -log = "0.4" -lsp-types = { version = "0.93", features = ["proposed"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "1.0" -tokio = { version = "1.24", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +anyhow = "1.0.68" +futures-executor = "0.3.25" +futures-util = { version = "0.3.25", features = ["std", "async-await"], default-features = false } +log = "0.4.17" +lsp-types = { version = "0.93.2", features = ["proposed"] } +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +thiserror = "1.0.38" +tokio = { version = "1.24.1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.11" -which = "4.2" +which = "4.3.0" diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 8418896c..5456cffe 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -39,6 +39,8 @@ pub enum Error { Timeout, #[error("server closed the stream")] StreamClosed, + #[error("LPS not defined")] + LspNotDefined, #[error("Unhandled")] Unhandled, #[error(transparent)] diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 895a0882..3d5ebae5 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -34,49 +34,49 @@ helix-dap = { version = "0.6", path = "../helix-dap" } helix-vcs = { version = "0.6", path = "../helix-vcs" } helix-loader = { version = "0.6", path = "../helix-loader" } -anyhow = "1" -once_cell = "1.17" +anyhow = "1.0.68" +once_cell = "1.17.0" -which = "4.2" +which = "4.3.0" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } +tokio = { version = "1.24.1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } -crossterm = { version = "0.25", features = ["event-stream"] } -signal-hook = "0.3" -tokio-stream = "0.1" -futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } -arc-swap = { version = "1.6.0" } +crossterm = { version = "0.25.0", features = ["event-stream"] } +signal-hook = "0.3.14" +tokio-stream = "0.1.11" +futures-util = { version = "0.3.25", features = ["std", "async-await"], default-features = false } +arc-swap = "1.6.0" # Logging -fern = "0.6" -chrono = { version = "0.4", default-features = false, features = ["clock"] } -log = "0.4" +fern = "0.6.1" +chrono = { version = "0.4.23", default-features = false, features = ["clock"] } +log = "0.4.17" # File picker -fuzzy-matcher = "0.3" -ignore = "0.4" +fuzzy-matcher = "0.3.7" +ignore = "0.4.19" # markdown doc rendering -pulldown-cmark = { version = "0.9", default-features = false } +pulldown-cmark = { version = "0.9.2", default-features = false } # file type detection content_inspector = "0.2.4" # config -toml = "0.5" +toml = "0.5.10" -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.91" +serde = { version = "1.0.152", features = ["derive"] } # ripgrep for global search -grep-regex = "0.1.10" -grep-searcher = "0.1.10" +grep-regex = "0.1.11" +grep-searcher = "0.1.11" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 -signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } +signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } [build-dependencies] helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] -smallvec = "1.10" +smallvec = "1.10.0" indoc = "1.0.8" tempfile = "3.3.0" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c0cbc245..62f87a5d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -21,11 +21,11 @@ use tui::backend::Backend; use crate::{ args::Args, commands::apply_workspace_edit, - compositor::{Compositor, Event}, + compositor::{self, Compositor, Event}, config::Config, job::Jobs, keymap::Keymaps, - ui::{self, overlay::overlayed}, + ui::{self, overlay::overlayed, Explorer}, }; use log::{debug, error, warn}; @@ -180,7 +180,19 @@ impl Application { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + let mut editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + + if args.show_explorer { + let mut jobs = Jobs::new(); + let mut context = compositor::Context { + editor: &mut editor, + scroll: None, + jobs: &mut jobs, + }; + let mut explorer = Explorer::new(&mut context)?; + explorer.unfocus(); + editor_view.explorer = Some(overlayed(explorer)); + } compositor.push(editor_view); if args.load_tutor { diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index dd787f1f..597a4688 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -17,6 +17,7 @@ pub struct Args { pub log_file: Option, pub config_file: Option, pub files: Vec<(PathBuf, Position)>, + pub show_explorer: bool, } impl Args { @@ -32,6 +33,7 @@ impl Args { "--version" => args.display_version = true, "--help" => args.display_help = true, "--tutor" => args.load_tutor = true, + "--show-explorer" => args.show_explorer = true, "--vsplit" => match args.split { Some(_) => anyhow::bail!("can only set a split once of a specific type"), None => args.split = Some(Layout::Vertical), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 09c2e5df..b4bfadbf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -272,6 +272,7 @@ impl MappableCommand { file_picker, "Open file picker", file_picker_in_current_directory, "Open file picker at current working directory", code_action, "Perform code action", + workspace_command_picker, "Open workspace command picker", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", symbol_picker, "Open symbol picker", @@ -444,7 +445,10 @@ impl MappableCommand { decrement, "Decrement item under cursor", record_macro, "Record macro", replay_macro, "Replay macro", - command_palette, "Open command palette", + command_palette, "Open command pallete", + toggle_or_focus_explorer, "toggle or focus explorer", + open_explorer_recursion, "open explorer recursion", + close_explorer, "close explorer", ); } @@ -2300,6 +2304,43 @@ fn file_picker_in_current_directory(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } +fn toggle_or_focus_explorer(cx: &mut Context) { + cx.callback = Some(Box::new( + |compositor: &mut Compositor, cx: &mut compositor::Context| { + if let Some(editor) = compositor.find::() { + match editor.explorer.as_mut() { + Some(explore) => explore.content.focus(), + None => match ui::Explorer::new(cx) { + Ok(explore) => editor.explorer = Some(overlayed(explore)), + Err(err) => cx.editor.set_error(format!("{}", err)), + }, + } + } + }, + )); +} + +fn open_explorer_recursion(cx: &mut Context) { + cx.callback = Some(Box::new( + |compositor: &mut Compositor, cx: &mut compositor::Context| { + if let Some(editor) = compositor.find::() { + match ui::Explorer::new_explorer_recursion() { + Ok(explore) => editor.explorer = Some(overlayed(explore)), + Err(err) => cx.editor.set_error(format!("{}", err)), + } + } + }, + )); +} + +fn close_explorer(cx: &mut Context) { + cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { + if let Some(editor) = compositor.find::() { + editor.explorer.take(); + } + })); +} + fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; @@ -2999,18 +3040,11 @@ pub mod insert { super::completion(cx); } - fn language_server_completion(cx: &mut Context, ch: char) { - let config = cx.editor.config(); - if !config.auto_completion { - return; - } - + fn is_server_trigger_char(doc: &Document, ch: char) -> bool { use helix_lsp::lsp; - // if ch matches completion char, trigger completion - let doc = doc_mut!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, + + let Some(language_server) = doc.language_server() else { + return false; }; let capabilities = language_server.capabilities(); @@ -3020,11 +3054,36 @@ pub mod insert { .. }) = &capabilities.completion_provider { - // TODO: what if trigger is multiple chars long - if triggers.iter().any(|trigger| trigger.contains(ch)) { - cx.editor.clear_idle_timer(); - super::completion(cx); + triggers.iter().any(|t| t.contains(ch)) + } else { + false + } + } + + fn language_server_completion(cx: &mut Context, ch: char) { + use helix_core::chars::char_is_word; + + let config = cx.editor.config(); + if !config.auto_completion { + return; + } + + let (view, doc) = current_ref!(cx.editor); + + if char_is_word(ch) && doc.savepoint.is_none() { + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + let mut chars = text.chars_at(cursor); + chars.reverse(); + + for _ in 0..config.completion_trigger_len { + if chars.next().map_or(true, |c| !char_is_word(c)) { + return; + } } + cx.editor.reset_idle_timer(); + } else if is_server_trigger_char(doc, ch) { + cx.editor.reset_idle_timer(); } } @@ -4029,9 +4088,15 @@ pub fn completion(cx: &mut Context) { let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); - let future = match language_server.completion(doc.identifier(), pos, None) { - Some(future) => future, - None => return, + let Some(future) = language_server.completion(doc.identifier(), pos, None) else { + return; + }; + let future = async move { + match future.await { + Ok(v) => Ok(v), + Err(helix_lsp::Error::Timeout) => Ok(serde_json::Value::Null), + Err(e) => Err(e), + } }; let trigger_offset = cursor; @@ -4044,29 +4109,54 @@ pub fn completion(cx: &mut Context) { iter.reverse(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let start_offset = cursor.saturating_sub(offset); + let prefix = text.slice(start_offset..cursor).to_string(); + + doc.savepoint(); + let trigger_version = doc.version(); cx.callback( future, move |editor, compositor, response: Option| { + let doc = doc_mut!(editor); + let Some(savepoint) = doc.savepoint.take() else { + return; + }; if editor.mode != Mode::Insert { // we're not in insert mode anymore return; } + if savepoint.0 != trigger_version { + doc.savepoint = Some(savepoint); + return; + } - let items = match response { + let mut items = match response { Some(lsp::CompletionResponse::Array(items)) => items, // TODO: do something with is_incomplete Some(lsp::CompletionResponse::List(lsp::CompletionList { is_incomplete: _is_incomplete, items, })) => items, - None => Vec::new(), + None => { + editor.set_status( + "The completion response is None. We will ask the server again", + ); + editor.reset_idle_timer(); + return; + } }; + if prefix.is_empty() { + items.retain(|item| match &item.filter_text { + Some(t) => t.starts_with(&prefix), + None => item.label.starts_with(&prefix), + }) + } if items.is_empty() { // editor.set_error("No completion available"); return; } + doc.savepoint = Some(savepoint); let size = compositor.size(); let ui = compositor.find::().unwrap(); ui.set_completion( diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 86b0c5fa..c19c536e 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -590,7 +590,6 @@ pub fn code_action(cx: &mut Context) { } // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec. - // Many details are modeled after vscode because langauge servers are usually tested against it. // VScode sorts the codeaction two times: // // First the codeactions that fix some diagnostics are moved to the front. @@ -624,7 +623,7 @@ pub fn code_action(cx: &mut Context) { .reverse() }); - let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { + let mut picker = ui::Menu::new(actions, true, (), move |editor, code_action, event| { if event != PromptEvent::Validate { return; } @@ -654,7 +653,7 @@ pub fn code_action(cx: &mut Context) { }); picker.move_down(); // pre-select the first item - let popup = Popup::new("code-action", picker).with_scrollbar(false); + let popup = Popup::new("code-action", picker); compositor.replace_or_push("code-action", popup); }, ) @@ -667,6 +666,34 @@ impl ui::menu::Item for lsp::Command { } } +pub fn workspace_command_picker(cx: &mut Context) { + let (_, doc) = current!(cx.editor); + + let language_server = language_server!(cx.editor, doc); + + let execute_command_provider = match &language_server.capabilities().execute_command_provider { + Some(p) => p, + None => return, + }; + let commands = execute_command_provider + .commands + .iter() + .map(|command| lsp::Command { + title: command.clone(), + command: command.clone(), + arguments: None, + }) + .collect::>(); + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, _cx: &mut compositor::Context| { + let picker = ui::Picker::new(commands, (), move |cx, command, _action| { + execute_lsp_command(cx.editor, command.clone()); + }); + compositor.push(Box::new(overlayed(picker))) + }, + )); +} + pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { let doc = doc!(editor); let language_server = language_server!(editor, doc); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index de24c4fb..4b0ec0e0 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -281,6 +281,31 @@ fn buffer_previous( Ok(()) } +fn delete( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let doc = doc_mut!(cx.editor); + + if doc.path().is_none() { + bail!("cannot delete a buffer with no associated file on the disk"); + } + + let future = doc.delete(); + cx.jobs.add(Job::new(future)); + + cx.block_try_flush_writes()?; + let doc_id = view!(cx.editor).doc; + cx.editor.close_document(doc_id, true)?; + + Ok(()) +} + fn write_impl( cx: &mut compositor::Context, path: Option<&Cow>, @@ -1924,6 +1949,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: new_file, completer: Some(completers::filename), }, + TypableCommand { + name: "delete", + aliases: &["remove", "rm", "del"], + doc: "Deletes the file associated with the current buffer", + fun: delete, + completer: None, + }, TypableCommand { name: "format", aliases: &["fmt"], @@ -2340,6 +2372,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: run_shell_command, completer: Some(completers::directory), }, + TypableCommand { + name: "lsp-restart", + aliases: &[], + doc: "Restarts the LSP server of the current buffer", + fun: lsp_restart, + completer: None, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index ef93dee0..5a7bd034 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -10,6 +10,8 @@ pub fn default() -> HashMap { "j" | "down" => move_line_down, "k" | "up" => move_line_up, "l" | "right" => move_char_right, + "C-j" => half_page_down, + "C-k" => half_page_up, "t" => find_till_char, "f" => find_next_char, @@ -175,7 +177,7 @@ pub fn default() -> HashMap { "C-u" => half_page_up, "C-d" => half_page_down, - "C-w" => { "Window" + "C-v" => { "View" "C-w" | "w" => rotate_view, "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, @@ -238,7 +240,8 @@ pub fn default() -> HashMap { "e" => dap_enable_exceptions, "E" => dap_disable_exceptions, }, - "w" => { "Window" + "w" => workspace_command_picker, + "v" => { "View" "C-w" | "w" => rotate_view, "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, @@ -270,6 +273,8 @@ pub fn default() -> HashMap { "r" => rename_symbol, "h" => select_references_to_symbol_under_cursor, "?" => command_palette, + "e" => toggle_or_focus_explorer, + "E" => close_explorer, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c537..a8c676b0 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -73,6 +73,7 @@ FLAGS: -V, --version Prints version information --vsplit Splits all given files vertically into different windows --hsplit Splits all given files horizontally into different windows + --show-explorer Opens the explorer on startup ", env!("CARGO_PKG_NAME"), VERSION_AND_GIT_HASH, diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 11d7886a..e24c0395 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -104,7 +104,7 @@ impl Completion { items.sort_by_key(|item| !item.preselect.unwrap_or(false)); // Then create the menu - let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, view_id: ViewId, @@ -462,6 +462,16 @@ impl Component for Completion { height = rel_height.min(height); } Rect::new(x, y, width, height) + } else if popup_x > 30 { + let mut height = area.height.saturating_sub(popup_y); + let mut width = popup_x; + if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { + width = rel_width.min(width); + height = rel_height.min(height); + } + let x = popup_x - width; + let y = popup_y; + Rect::new(x, y, width, height) } else { let half = area.height / 2; let height = 15.min(half); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 35cf77ab..1412b6cd 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -4,7 +4,7 @@ use crate::{ job::{self, Callback}, key, keymap::{KeymapResult, Keymaps}, - ui::{Completion, ProgressSpinners}, + ui::{overlay::Overlay, Completion, Explorer, ProgressSpinners}, }; use helix_core::{ @@ -19,7 +19,7 @@ use helix_core::{ use helix_view::{ apply_transaction, document::{Mode, SCRATCH_BUFFER_NAME}, - editor::{CompleteAction, CursorShapeConfig}, + editor::{CompleteAction, CursorShapeConfig, RainbowIndentOptions}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, @@ -39,6 +39,7 @@ pub struct EditorView { last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, + pub(crate) explorer: Option>, } #[derive(Debug, Clone)] @@ -63,6 +64,7 @@ impl EditorView { last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), + explorer: None, } } @@ -462,6 +464,27 @@ impl EditorView { let starting_indent = (offset.col / tab_width) + config.indent_guides.skip_levels as usize; + let modifier = if config.indent_guides.rainbow == RainbowIndentOptions::Dim { + Modifier::DIM + } else { + Modifier::empty() + }; + + for i in starting_indent..(indent_level / tab_width) { + let style = if config.indent_guides.rainbow != RainbowIndentOptions::None { + indent_guide_style + .patch(theme.get_rainbow(i as usize)) + .add_modifier(modifier) + } else { + indent_guide_style + }; + surface.set_string( + viewport.x + (i as u16 * tab_width as u16) - offset.col as u16, + viewport.y + line, + &indent_guide_char, + style, + ); + } // Don't draw indent guides outside of view let end_indent = min( indent_level, @@ -934,7 +957,7 @@ impl EditorView { } (Mode::Insert, Mode::Normal) => { // if exiting insert mode, remove completion - self.completion = None; + self.clear_completion(cxt.editor); // TODO: Use an on_mode_change hook to remove signature help cxt.jobs.callback(async { @@ -1072,9 +1095,6 @@ impl EditorView { return; } - // Immediately initialize a savepoint - doc_mut!(editor).savepoint(); - editor.last_completion = None; self.last_insert.1.push(InsertEvent::TriggerCompletion); @@ -1105,7 +1125,15 @@ impl EditorView { return EventResult::Ignored(None); } - crate::commands::insert::idle_completion(cx); + let mut cx = commands::Context { + register: None, + editor: cx.editor, + jobs: cx.jobs, + count: None, + callback: None, + on_next_key_callback: None, + }; + crate::commands::insert::idle_completion(&mut cx); EventResult::Consumed(None) } @@ -1299,6 +1327,11 @@ impl Component for EditorView { event: &Event, context: &mut crate::compositor::Context, ) -> EventResult { + if let Some(explore) = self.explorer.as_mut() { + if let EventResult::Consumed(callback) = explore.handle_event(event, context) { + return EventResult::Consumed(callback); + } + } let mut cx = commands::Context { editor: context.editor, count: None, @@ -1333,7 +1366,7 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(mut key) => { - cx.editor.reset_idle_timer(); + cx.editor.clear_idle_timer(); canonicalize_key(&mut key); // clear status @@ -1385,7 +1418,8 @@ impl Component for EditorView { if let Some(completion) = &mut self.completion { completion.update(&mut cx); if completion.is_empty() { - self.clear_completion(cx.editor); + self.completion = None; + doc_mut!(cx.editor).savepoint = None; } } } @@ -1462,6 +1496,21 @@ impl Component for EditorView { } // if the terminal size suddenly changed, we need to trigger a resize + if self.explorer.is_some() && (config.explorer.is_embed()) { + editor_area = editor_area.clip_left(config.explorer.column_width as u16 + 2); + } + cx.editor.resize(editor_area); // -1 from bottom for commandline + + if let Some(explore) = self.explorer.as_mut() { + if !explore.content.is_focus() && config.explorer.is_embed() { + let current_doc = view!(cx.editor).doc; + let current_doc = cx.editor.document(current_doc).unwrap(); + if let Some(path) = current_doc.path() { + explore.content.set_selection(&path); + } + explore.content.render(area, surface, cx); + } + } cx.editor.resize(editor_area); if use_bufferline { @@ -1542,9 +1591,30 @@ impl Component for EditorView { if let Some(completion) = self.completion.as_mut() { completion.render(area, surface, cx); } + + if let Some(explore) = self.explorer.as_mut() { + if explore.content.is_focus() { + if config.explorer.is_embed() { + explore.content.render(area, surface, cx); + } else { + explore.render(area, surface, cx); + } + } + } } fn cursor(&self, _area: Rect, editor: &Editor) -> (Option, CursorKind) { + if let Some(explore) = &self.explorer { + if explore.content.is_focus() { + if editor.config().explorer.is_overlay() { + return explore.cursor(_area, editor); + } + let cursor = explore.content.cursor(_area, editor); + if cursor.0.is_some() { + return cursor; + } + } + } match editor.cursor() { // All block cursors are drawn manually (pos, CursorKind::Block) => (pos, CursorKind::Hidden), diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs new file mode 100644 index 00000000..83250460 --- /dev/null +++ b/helix-term/src/ui/explore.rs @@ -0,0 +1,914 @@ +use super::{Prompt, Tree, TreeItem, TreeOp}; +use crate::{ + compositor::{Component, Compositor, Context, EventResult}, + ctrl, key, shift, ui, +}; +use anyhow::{bail, ensure, Result}; +use helix_core::Position; +use helix_view::{ + editor::Action, + graphics::{CursorKind, Modifier, Rect}, + input::{Event, KeyEvent}, + Editor, +}; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::path::{Path, PathBuf}; +use tui::{ + buffer::Buffer as Surface, + text::{Span, Spans}, + widgets::{Block, Borders, Widget}, +}; + +macro_rules! get_theme { + ($theme: expr, $s1: expr, $s2: expr) => { + $theme.try_get($s1).unwrap_or_else(|| $theme.get($s2)) + }; +} + +const ICONS: &'static [&'static str] = + &["", "", "", "", "", "ﰟ", "", "", "", "ﯤ", "", "ﬥ"]; + +const ICONS_EXT: &'static [&'static str] = &[ + ".rs", ".md", ".js", ".c", ".png", ".svg", ".css", ".html", ".lua", ".ts", ".py", ".json", +]; + +const ICONS_COLORS: &'static [helix_view::theme::Color] = &[ + helix_view::theme::Color::Rgb(227, 134, 84), + helix_view::theme::Color::LightCyan, + helix_view::theme::Color::Yellow, + helix_view::theme::Color::Blue, + helix_view::theme::Color::Yellow, + helix_view::theme::Color::Yellow, + helix_view::theme::Color::Green, + helix_view::theme::Color::Blue, + helix_view::theme::Color::Red, + helix_view::theme::Color::Blue, + helix_view::theme::Color::Red, +]; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum FileType { + File, + Dir, + Exe, + Placeholder, + Parent, + Root, +} + +#[derive(Debug, Clone)] +struct FileInfo { + file_type: FileType, + expanded: bool, + path: PathBuf, +} + +impl FileInfo { + fn new(path: PathBuf, file_type: FileType) -> Self { + Self { + path, + file_type, + expanded: false, + } + } + + fn root(path: PathBuf) -> Self { + Self { + file_type: FileType::Root, + path, + expanded: true, + } + } + + fn parent(path: &Path) -> Self { + let p = path.parent().unwrap_or_else(|| Path::new("")); + Self { + file_type: FileType::Parent, + path: p.to_path_buf(), + expanded: false, + } + } + + fn get_text(&self) -> Cow<'static, str> { + match self.file_type { + FileType::Parent => "..".into(), + FileType::Placeholder => "---".into(), + FileType::Root => { + if let Some(path) = self.path.iter().last() { + format!("- {} -", path.to_string_lossy()).into() + } else { + Cow::from("/") + } + } + FileType::File | FileType::Exe | FileType::Dir => self + .path + .file_name() + .map_or("/".into(), |p| p.to_string_lossy().into_owned().into()), + } + } +} + +impl TreeItem for FileInfo { + type Params = State; + fn text(&self, cx: &mut Context, selected: bool, state: &mut State) -> Spans { + let text = self.get_text(); + let theme = &cx.editor.theme; + + let style = match self.file_type { + FileType::Parent | FileType::Dir | FileType::Root => "ui.explorer.dir", + FileType::File | FileType::Exe | FileType::Placeholder => "ui.explorer.file", + }; + let mut style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text")); + if selected { + let patch = match state.focus { + true => "ui.explorer.focus", + false => "ui.explorer.unfocus", + }; + if let Some(patch) = theme.try_get(patch) { + style = style.patch(patch); + } else { + style = style.add_modifier(Modifier::REVERSED); + } + } + Spans::from(Span::styled(text, style)) + } + + fn is_child(&self, other: &Self) -> bool { + if let FileType::Parent = other.file_type { + return false; + } + if let FileType::Placeholder = self.file_type { + self.path == other.path + } else { + self.path.parent().map_or(false, |p| p == other.path) + } + } + + fn cmp(&self, other: &Self) -> Ordering { + use FileType::*; + match (self.file_type, other.file_type) { + (Parent, _) => return Ordering::Less, + (_, Parent) => return Ordering::Greater, + (Root, _) => return Ordering::Less, + (_, Root) => return Ordering::Greater, + _ => {} + }; + + if self.path == other.path { + match (self.file_type, other.file_type) { + (_, Placeholder) => return Ordering::Less, + (Placeholder, _) => return Ordering::Greater, + _ => {} + }; + } + + if let (Some(p1), Some(p2)) = (self.path.parent(), other.path.parent()) { + if p1 == p2 { + match (self.file_type, other.file_type) { + (Dir, File | Exe) => return Ordering::Less, + (File | Exe, Dir) => return Ordering::Greater, + _ => {} + }; + } + } + self.path.cmp(&other.path) + } + + fn get_childs(&self) -> Result> { + match self.file_type { + FileType::Root | FileType::Dir => {} + _ => return Ok(vec![]), + }; + let mut ret: Vec<_> = std::fs::read_dir(&self.path)? + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + entry.metadata().ok().map(|meta| { + let is_exe = false; + let file_type = match (meta.is_dir(), is_exe) { + (true, _) => FileType::Dir, + (_, false) => FileType::File, + (_, true) => FileType::Exe, + }; + Self { + file_type, + path: self.path.join(entry.file_name()), + expanded: false, + } + }) + }) + .collect(); + if ret.is_empty() { + ret.push(Self { + path: self.path.clone(), + file_type: FileType::Placeholder, + expanded: false, + }) + } + Ok(ret) + } + + fn filter(&self, _cx: &mut Context, s: &str, _params: &mut Self::Params) -> bool { + if s.is_empty() { + false + } else { + self.get_text().contains(s) + } + } + + fn icon(&self) -> Option<(&'static str, &'static helix_view::theme::Color)> { + return match self.file_type { + FileType::Dir => { + if self.expanded { + Some(("", &helix_view::theme::Color::Yellow)) + } else { + Some(("", &helix_view::theme::Color::Yellow)) + } + } + FileType::File => { + for (i, ext) in ICONS_EXT.iter().enumerate() { + if self.get_text().ends_with(ext) { + let color = ICONS_COLORS + .iter() + .nth(i) + .unwrap_or(&helix_view::theme::Color::Blue); + return ICONS.iter().nth(i).map(|c| (*c, color)); + } + } + return Some(("", &helix_view::theme::Color::LightBlue)); + } + _ => None, + }; + } +} + +#[derive(Clone, Copy, Debug)] +enum PromptAction { + Search(bool), // search next/search pre + Mkdir, + CreateFile, + RemoveDir, + RemoveFile, + Filter, +} + +#[derive(Clone, Debug)] +struct State { + focus: bool, + current_root: PathBuf, +} + +impl State { + fn new(focus: bool, current_root: PathBuf) -> Self { + Self { + focus, + current_root, + } + } +} + +pub struct Explorer { + tree: Tree, + state: State, + prompt: Option<(PromptAction, Prompt)>, + #[allow(clippy::type_complexity)] + on_next_key: Option EventResult>>, + #[allow(clippy::type_complexity)] + repeat_motion: Option>, +} + +impl Explorer { + pub fn new(cx: &mut Context) -> Result { + let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); + let items = Self::get_items(current_root.clone(), cx)?; + Ok(Self { + tree: Tree::build_tree(items) + .with_enter_fn(Self::toggle_current) + .with_folded_fn(Self::fold_current), + state: State::new(true, current_root), + repeat_motion: None, + prompt: None, + on_next_key: None, + }) + } + + pub fn set_selection(&mut self, path: &Path) { + let info = if path.is_file() { + FileInfo::new(path.into(), FileType::File) + } else { + FileInfo::new(path.into(), FileType::Dir) + }; + self.tree.select(&info); + self.tree.save_view(); + } + + pub fn new_explorer_recursion() -> Result { + let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); + let parent = FileInfo::parent(¤t_root); + let root = FileInfo::root(current_root.clone()); + let mut tree = Tree::build_from_root(root, usize::MAX / 2)? + .with_enter_fn(Self::toggle_current) + .with_folded_fn(Self::fold_current); + tree.insert_current_level(parent); + Ok(Self { + tree, + state: State::new(true, current_root), + repeat_motion: None, + prompt: None, + on_next_key: None, + }) + } + + pub fn focus(&mut self) { + self.state.focus = true + } + + pub fn unfocus(&mut self) { + self.state.focus = false; + } + + pub fn is_focus(&self) -> bool { + self.state.focus + } + + fn get_items(p: PathBuf, cx: &mut Context) -> Result> { + let mut items = Vec::new(); + let root = FileInfo::root(p); + let childs = root.get_childs()?; + if cx.editor.config().explorer.is_tree() { + items.push(root) + } + items.extend(childs); + Ok(items) + } + + fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) { + if area.height <= 2 || area.width < 60 { + return; + } + let item = self.tree.current().item(); + if item.file_type == FileType::Placeholder { + return; + } + let head_area = render_block( + area.clip_bottom(area.height - 2), + surface, + Borders::BOTTOM, + None, + ); + let path_str = format!("{}", item.path.display()); + surface.set_stringn( + head_area.x, + head_area.y, + path_str, + head_area.width as usize, + get_theme!(editor.theme, "ui.explorer.dir", "ui.text"), + ); + + let body_area = area.clip_top(2); + let style = editor.theme.get("ui.text"); + if let Ok(preview_content) = get_preview(&item.path, body_area.height as usize) { + preview_content + .into_iter() + .enumerate() + .for_each(|(row, line)| { + surface.set_stringn( + body_area.x, + body_area.y + row as u16, + line, + body_area.width as usize, + style, + ); + }) + } + } + + fn new_search_prompt(&mut self, search_next: bool) { + self.tree.save_view(); + self.prompt = Some(( + PromptAction::Search(search_next), + Prompt::new("search: ".into(), None, ui::completers::none, |_, _, _| {}), + )) + } + + fn new_filter_prompt(&mut self) { + self.tree.save_view(); + self.prompt = Some(( + PromptAction::Filter, + Prompt::new("filter: ".into(), None, ui::completers::none, |_, _, _| {}), + )) + } + + fn new_mkdir_prompt(&mut self) { + self.prompt = Some(( + PromptAction::Mkdir, + Prompt::new("mkdir: ".into(), None, ui::completers::none, |_, _, _| {}), + )); + } + + fn new_create_file_prompt(&mut self) { + self.prompt = Some(( + PromptAction::CreateFile, + Prompt::new( + "create file: ".into(), + None, + ui::completers::none, + |_, _, _| {}, + ), + )); + } + + fn new_remove_file_prompt(&mut self, cx: &mut Context) { + let item = self.tree.current_item(); + let check = || { + ensure!(item.file_type != FileType::Placeholder, "The path is empty"); + ensure!( + item.file_type != FileType::Parent, + "can not remove parent dir" + ); + ensure!(item.path.is_file(), "The path is not a file"); + let doc = cx.editor.document_by_path(&item.path); + ensure!(doc.is_none(), "The file is opened"); + Ok(()) + }; + if let Err(e) = check() { + cx.editor.set_error(format!("{e}")); + return; + } + let p = format!("remove file: {}, YES? ", item.path.display()); + self.prompt = Some(( + PromptAction::RemoveFile, + Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), + )); + } + + fn new_remove_dir_prompt(&mut self, cx: &mut Context) { + let item = self.tree.current_item(); + let check = || { + ensure!(item.file_type != FileType::Placeholder, "The path is empty"); + ensure!( + item.file_type != FileType::Parent, + "can not remove parent dir" + ); + ensure!(item.path.is_dir(), "The path is not a dir"); + let doc = cx.editor.documents().find(|doc| { + doc.path() + .map(|p| p.starts_with(&item.path)) + .unwrap_or(false) + }); + ensure!(doc.is_none(), "There are files opened under the dir"); + Ok(()) + }; + if let Err(e) = check() { + cx.editor.set_error(format!("{e}")); + return; + } + let p = format!("remove dir: {}, YES? ", item.path.display()); + self.prompt = Some(( + PromptAction::RemoveDir, + Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), + )); + } + + fn fold_current(item: &mut FileInfo, _cx: &mut Context, _state: &mut State) { + if item.path.is_dir() && item.file_type != FileType::Root { + item.expanded = false; + } + } + + fn toggle_current( + item: &mut FileInfo, + cx: &mut Context, + state: &mut State, + ) -> TreeOp { + if item.file_type == FileType::Placeholder { + return TreeOp::Noop; + } + if item.path == Path::new("") { + return TreeOp::Noop; + } + let meta = match std::fs::metadata(&item.path) { + Ok(meta) => meta, + Err(e) => { + cx.editor.set_error(format!("{e}")); + return TreeOp::Noop; + } + }; + if meta.is_file() { + if let Err(e) = cx.editor.open(&item.path.clone(), Action::Replace) { + cx.editor.set_error(format!("{e}")); + } + state.focus = false; + return TreeOp::Noop; + } + + if item.path.is_dir() { + item.expanded = true; + if cx.editor.config().explorer.is_list() || item.file_type == FileType::Parent { + match Self::get_items(item.path.clone(), cx) { + Ok(items) => { + state.current_root = item.path.clone(); + return TreeOp::ReplaceTree(items); + } + Err(e) => cx.editor.set_error(format!("{e}")), + } + } else { + return TreeOp::GetChildsAndInsert; + } + } + cx.editor.set_error("unkonw file type"); + TreeOp::Noop + } + + fn render_float(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let background = cx.editor.theme.get("ui.background"); + let column_width = cx.editor.config().explorer.column_width as u16; + surface.clear_with(area, background); + let area = render_block(area, surface, Borders::ALL, None); + + let mut preview_area = area.clip_left(column_width + 1); + if let Some((_, prompt)) = self.prompt.as_mut() { + let area = preview_area.clip_bottom(2); + let promp_area = render_block( + preview_area.clip_top(area.height), + surface, + Borders::TOP, + None, + ); + prompt.render(promp_area, surface, cx); + preview_area = area; + } + self.render_preview(preview_area, surface, cx.editor); + + let list_area = render_block( + area.clip_right(preview_area.width), + surface, + Borders::RIGHT, + None, + ); + self.tree.render(list_area, surface, cx, &mut self.state); + } + + fn render_embed(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let config = &cx.editor.config().explorer; + let side_area = area + .with_width(area.width.min(config.column_width as u16 + 2)) + .clip_bottom(1); + + let background = cx.editor.theme.get("ui.statusline"); + surface.clear_with(side_area, background); + + let preview_area = area.clip_left(side_area.width).clip_bottom(2); + let prompt_area = area.clip_top(side_area.height); + + let border_style = cx.editor.theme.get("ui.explorer.border"); + let list_area = render_block( + side_area.clip_left(1), + surface, + Borders::RIGHT, + Some(border_style), + ) + .clip_bottom(1); + self.tree.render(list_area, surface, cx, &mut self.state); + + { + let statusline = if self.is_focus() { + cx.editor.theme.get("ui.statusline") + } else { + cx.editor.theme.get("ui.statusline.inactive") + }; + let area = side_area.clip_top(list_area.height).clip_right(1); + surface.clear_with(area, statusline); + } + + if self.is_focus() { + if preview_area.width < 30 || preview_area.height < 3 { + return; + } + let width = preview_area.width.min(90); + let mut y = self.tree.row().saturating_sub(1) as u16; + let height = (preview_area.height).min(25); + if (height + y) > preview_area.height { + y = preview_area.height - height; + } + let area = Rect::new(preview_area.x, y, width, height); + surface.clear_with(area, background); + let area = render_block(area, surface, Borders::all(), None); + self.render_preview(area, surface, cx.editor); + } + + if let Some((_, prompt)) = self.prompt.as_mut() { + prompt.render_prompt(prompt_area, surface, cx) + } + } + + fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { + let (action, mut prompt) = self.prompt.take().unwrap(); + match event { + key!(Tab) | key!(Down) | ctrl!('j') => { + self.tree.clean_recycle(); + return self + .tree + .handle_event(Event::Key(event.clone()), cx, &mut self.state); + } + key!(Enter) => { + self.tree.clean_recycle(); + return self + .tree + .handle_event(Event::Key(event.clone()), cx, &mut self.state); + } + key!(Esc) | ctrl!('c') => self.tree.restore_recycle(), + _ => { + if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { + self.tree.filter(prompt.line(), cx, &mut self.state); + } + self.prompt = Some((action, prompt)); + } + }; + EventResult::Consumed(None) + } + + fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { + let (action, mut prompt) = self.prompt.take().unwrap(); + let search_next = match action { + PromptAction::Search(search_next) => search_next, + _ => return EventResult::Ignored(None), + }; + match event { + key!(Tab) | key!(Down) | ctrl!('j') => { + return self + .tree + .handle_event(Event::Key(event.clone()), cx, &mut self.state) + } + key!(Enter) => { + let search_str = prompt.line().clone(); + if !search_str.is_empty() { + self.repeat_motion = Some(Box::new(move |explorer, action, cx| { + if let PromptAction::Search(is_next) = action { + explorer.tree.save_view(); + if is_next == search_next { + explorer + .tree + .search_next(cx, &search_str, &mut explorer.state); + } else { + explorer + .tree + .search_pre(cx, &search_str, &mut explorer.state); + } + } + })) + } else { + self.repeat_motion = None; + } + return self + .tree + .handle_event(Event::Key(event.clone()), cx, &mut self.state); + } + key!(Esc) | ctrl!('c') => self.tree.restore_view(), + _ => { + if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { + if search_next { + self.tree.search_next(cx, prompt.line(), &mut self.state); + } else { + self.tree.search_pre(cx, prompt.line(), &mut self.state); + } + } + self.prompt = Some((action, prompt)); + } + }; + EventResult::Consumed(None) + } + + fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { + match &self.prompt { + Some((PromptAction::Search(_), _)) => return self.handle_search_event(event, cx), + Some((PromptAction::Filter, _)) => return self.handle_filter_event(event, cx), + _ => {} + }; + let (action, mut prompt) = match self.prompt.take() { + Some((action, p)) => (action, p), + _ => return EventResult::Ignored(None), + }; + let line = prompt.line(); + match (action, event) { + (PromptAction::Mkdir, key!(Enter)) => { + if let Err(e) = self.new_path(line, true) { + cx.editor.set_error(format!("{e}")) + } + } + (PromptAction::CreateFile, key!(Enter)) => { + if let Err(e) = self.new_path(line, false) { + cx.editor.set_error(format!("{e}")) + } + } + (PromptAction::RemoveDir, key!(Enter)) => { + let item = self.tree.current_item(); + if let Err(e) = std::fs::remove_dir_all(&item.path) { + cx.editor.set_error(format!("{e}")); + } else { + self.tree.fold_current_child(); + self.tree.remove_current(); + } + } + (PromptAction::RemoveFile, key!(Enter)) => { + if line == "YES" { + let item = self.tree.current_item(); + if let Err(e) = std::fs::remove_file(&item.path) { + cx.editor.set_error(format!("{e}")); + } else { + self.tree.remove_current(); + } + } + } + (_, key!(Esc) | ctrl!('c')) => {} + _ => { + prompt.handle_event(&Event::Key(*event), cx); + self.prompt = Some((action, prompt)); + } + } + EventResult::Consumed(None) + } + + fn new_path(&mut self, file_name: &str, is_dir: bool) -> Result<()> { + let current = self.tree.current_item(); + let current_parent = if current.file_type == FileType::Placeholder { + ¤t.path + } else { + current + .path + .parent() + .ok_or_else(|| anyhow::anyhow!("can not get parent dir"))? + }; + let p = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); + match p.parent() { + Some(p) if p == current_parent => {} + _ => bail!("The file name is not illegal"), + }; + + let f = if is_dir { + std::fs::create_dir(&p)?; + FileInfo::new(p, FileType::Dir) + } else { + let mut fd = std::fs::OpenOptions::new(); + fd.create_new(true).write(true).open(&p)?; + FileInfo::new(p, FileType::File) + }; + if current.file_type == FileType::Placeholder { + self.tree.replace_current(f); + } else { + self.tree.insert_current_level(f); + } + Ok(()) + } +} + +impl Component for Explorer { + /// Process input events, return true if handled. + fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { + let key_event = match event { + Event::Key(event) => *event, + Event::Resize(..) => return EventResult::Consumed(None), + _ => return EventResult::Ignored(None), + }; + if !self.is_focus() { + return EventResult::Ignored(None); + } + if let Some(mut on_next_key) = self.on_next_key.take() { + return on_next_key(cx, self, &key_event); + } + + if let EventResult::Consumed(c) = self.handle_prompt_event(&key_event, cx) { + return EventResult::Consumed(c); + } + + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { + if let Some(editor) = compositor.find::() { + editor.explorer = None; + } + }))); + + match key_event { + key!(Esc) => self.unfocus(), + ctrl!('c') => return close_fn, + key!('n') => { + if let Some(mut repeat_motion) = self.repeat_motion.take() { + repeat_motion(self, PromptAction::Search(true), cx); + self.repeat_motion = Some(repeat_motion); + } + } + shift!('N') => { + if let Some(mut repeat_motion) = self.repeat_motion.take() { + repeat_motion(self, PromptAction::Search(false), cx); + self.repeat_motion = Some(repeat_motion); + } + } + key!('f') => self.new_filter_prompt(), + key!('/') => self.new_search_prompt(true), + key!('?') => self.new_search_prompt(false), + key!('m') => { + self.on_next_key = Some(Box::new(|_, explorer, event| { + match event { + key!('d') => explorer.new_mkdir_prompt(), + key!('f') => explorer.new_create_file_prompt(), + _ => return EventResult::Ignored(None), + }; + EventResult::Consumed(None) + })); + } + key!('r') => { + self.on_next_key = Some(Box::new(|cx, explorer, event| { + match event { + key!('d') => explorer.new_remove_dir_prompt(cx), + key!('f') => explorer.new_remove_file_prompt(cx), + _ => return EventResult::Ignored(None), + }; + EventResult::Consumed(None) + })); + } + _ => { + self.tree + .handle_event(Event::Key(key_event.clone()), cx, &mut self.state); + } + } + + EventResult::Consumed(None) + } + + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + if area.width < 10 || area.height < 5 { + cx.editor.set_error("explorer render area is too small"); + return; + } + let config = &cx.editor.config().explorer; + if config.is_embed() { + self.render_embed(area, surface, cx); + } else { + self.render_float(area, surface, cx); + } + } + + fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { + let prompt = match self.prompt.as_ref() { + Some((_, prompt)) => prompt, + None => return (None, CursorKind::Hidden), + }; + let config = &editor.config().explorer; + let (x, y) = if config.is_overlay() { + let colw = config.column_width as u16; + if area.width > colw { + (area.x + colw + 2, area.y + area.height - 2) + } else { + return (None, CursorKind::Hidden); + } + } else { + (area.x, area.y + area.height - 1) + }; + prompt.cursor(Rect::new(x, y, area.width, 1), editor) + } +} + +fn get_preview(p: impl AsRef, max_line: usize) -> Result> { + let p = p.as_ref(); + if p.is_dir() { + return Ok(p + .read_dir()? + .filter_map(|entry| entry.ok()) + .take(max_line) + .map(|entry| { + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + format!("{}/", entry.file_name().to_string_lossy()) + } else { + format!("{}", entry.file_name().to_string_lossy()) + } + }) + .collect()); + } + + ensure!(p.is_file(), "path: {} is not file or dir", p.display()); + use std::fs::OpenOptions; + use std::io::BufRead; + let mut fd = OpenOptions::new(); + fd.read(true); + let fd = fd.open(p)?; + Ok(std::io::BufReader::new(fd) + .lines() + .take(max_line) + .filter_map(|line| line.ok()) + .map(|line| line.replace('\t', " ")) + .collect()) +} + +fn render_block( + area: Rect, + surface: &mut Surface, + borders: Borders, + border_style: Option, +) -> Rect { + let mut block = Block::default().borders(borders); + if let Some(style) = border_style { + block = block.border_style(style); + } + let inner = block.inner(area); + block.render(area, surface); + inner +} diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index b9c1f9de..1e9b08e7 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -74,11 +74,12 @@ impl Menu { // rendering) pub fn new( options: Vec, + sort: bool, editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { let matches = (0..options.len()).map(|i| (i, 0)).collect(); - Self { + let mut menu = Self { options, editor_data, matcher: Box::new(Matcher::default()), @@ -90,7 +91,16 @@ impl Menu { size: (0, 0), viewport: (0, 0), recalculate: true, + }; + + if sort { + // TODO: scoring on empty input should just use a fastpath + menu.score(""); + } else { + menu.matches = (0..menu.options.len()).map(|i| (i, 0)).collect(); } + + menu } pub fn score(&mut self, pattern: &str) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index eb480758..ea6d9b7e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,5 +1,6 @@ mod completion; pub(crate) mod editor; +mod explore; mod fuzzy_match; mod info; pub mod lsp; @@ -12,11 +13,13 @@ mod prompt; mod spinner; mod statusline; mod text; +mod tree; use crate::compositor::{Component, Compositor}; use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; +pub use explore::Explorer; pub use markdown::Markdown; pub use menu::Menu; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; @@ -24,6 +27,7 @@ pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; +pub use tree::{Tree, TreeItem, TreeOp}; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs new file mode 100644 index 00000000..6c643b8f --- /dev/null +++ b/helix-term/src/ui/tree.rs @@ -0,0 +1,707 @@ +use std::cmp::Ordering; +use std::iter::Peekable; + +use anyhow::Result; + +use crate::{ + compositor::{Context, EventResult}, + ctrl, key, shift, +}; +use helix_core::unicode::width::UnicodeWidthStr; +use helix_view::{ + graphics::Rect, + input::{Event, KeyEvent}, +}; +use tui::{buffer::Buffer as Surface, text::Spans}; + +pub trait TreeItem: Sized { + type Params; + + fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; + fn is_child(&self, other: &Self) -> bool; + fn cmp(&self, other: &Self) -> Ordering; + fn icon(&self) -> Option<(&'static str, &'static helix_view::theme::Color)>; + + fn filter(&self, cx: &mut Context, s: &str, params: &mut Self::Params) -> bool { + self.text(cx, false, params) + .0 + .into_iter() + .map(|s| s.content) + .collect::>() + .concat() + .contains(s) + } + + fn get_childs(&self) -> Result> { + Ok(vec![]) + } +} + +fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { + if item1.is_child(item2) { + return Ordering::Greater; + } + if item2.is_child(item1) { + return Ordering::Less; + } + + T::cmp(item1, item2) +} + +fn vec_to_tree(mut items: Vec, level: usize) -> Vec> { + fn get_childs(iter: &mut Peekable, elem: &mut Elem) + where + T: TreeItem, + Iter: Iterator, + { + let level = elem.level + 1; + loop { + if !iter.peek().map_or(false, |next| next.is_child(&elem.item)) { + break; + } + let mut child = Elem::new(iter.next().unwrap(), level); + if iter.peek().map_or(false, |nc| nc.is_child(&child.item)) { + get_childs(iter, &mut child); + } + elem.folded.push(child); + } + } + + items.sort_by(tree_item_cmp); + let mut elems = Vec::with_capacity(items.len()); + let mut iter = items.into_iter().peekable(); + while let Some(item) = iter.next() { + let mut elem = Elem::new(item, level); + if iter.peek().map_or(false, |next| next.is_child(&elem.item)) { + get_childs(&mut iter, &mut elem); + } + expand_elems(&mut elems, elem); + } + elems +} + +// return total elems's count contain self +fn get_elems_recursion(t: &mut Elem, depth: usize) -> Result { + let mut childs = t.item.get_childs()?; + childs.sort_by(tree_item_cmp); + let mut elems = Vec::with_capacity(childs.len()); + let level = t.level + 1; + let mut total = 1; + for child in childs { + let mut elem = Elem::new(child, level); + let count = if depth > 0 { + get_elems_recursion(&mut elem, depth - 1)? + } else { + 1 + }; + elems.push(elem); + total += count; + } + t.folded = elems; + Ok(total) +} + +fn expand_elems(dist: &mut Vec>, mut t: Elem) { + let childs = std::mem::take(&mut t.folded); + dist.push(t); + for child in childs { + expand_elems(dist, child) + } +} + +pub enum TreeOp { + Noop, + Restore, + InsertChild(Vec), + GetChildsAndInsert, + ReplaceTree(Vec), +} + +pub struct Elem { + item: T, + level: usize, + folded: Vec, +} + +impl Clone for Elem { + fn clone(&self) -> Self { + Self { + item: self.item.clone(), + level: self.level, + folded: self.folded.clone(), + } + } +} + +impl Elem { + pub fn new(item: T, level: usize) -> Self { + Self { + item, + level, + folded: vec![], + } + } + + pub fn item(&self) -> &T { + &self.item + } +} + +pub struct Tree { + items: Vec>, + recycle: Option<(String, Vec>)>, + selected: usize, + save_view: (usize, usize), // (selected, row) + row: usize, + col: usize, + max_len: usize, + count: usize, + tree_symbol_style: String, + #[allow(clippy::type_complexity)] + pre_render: Option>, + #[allow(clippy::type_complexity)] + on_opened_fn: + Option TreeOp + 'static>>, + #[allow(clippy::type_complexity)] + on_folded_fn: Option>, + #[allow(clippy::type_complexity)] + on_next_key: Option>, +} + +impl Tree { + pub fn new(items: Vec>) -> Self { + Self { + items, + recycle: None, + selected: 0, + save_view: (0, 0), + row: 0, + col: 0, + max_len: 0, + count: 0, + tree_symbol_style: "ui.explorer.guide".into(), + pre_render: None, + on_opened_fn: None, + on_folded_fn: None, + on_next_key: None, + } + } + + pub fn replace_with_new_items(&mut self, items: Vec) { + let old = std::mem::replace(self, Self::new(vec_to_tree(items, 0))); + self.on_opened_fn = old.on_opened_fn; + self.on_folded_fn = old.on_folded_fn; + self.tree_symbol_style = old.tree_symbol_style; + } + + pub fn build_tree(items: Vec) -> Self { + Self::new(vec_to_tree(items, 0)) + } + + pub fn build_from_root(t: T, depth: usize) -> Result { + let mut elem = Elem::new(t, 0); + let count = get_elems_recursion(&mut elem, depth)?; + let mut elems = Vec::with_capacity(count); + expand_elems(&mut elems, elem); + Ok(Self::new(elems)) + } + + pub fn with_enter_fn(mut self, f: F) -> Self + where + F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static, + { + self.on_opened_fn = Some(Box::new(f)); + self + } + + pub fn with_folded_fn(mut self, f: F) -> Self + where + F: FnMut(&mut T, &mut Context, &mut T::Params) + 'static, + { + self.on_folded_fn = Some(Box::new(f)); + self + } + + pub fn tree_symbol_style(mut self, style: String) -> Self { + self.tree_symbol_style = style; + self + } + + fn next_item(&self) -> Option<&Elem> { + self.items.get(self.selected + 1) + } + + fn next_not_descendant_pos(&self, index: usize) -> usize { + let item = &self.items[index]; + self.find(index + 1, false, |n| n.level <= item.level) + .unwrap_or(self.items.len()) + } + + fn find_parent(&self, index: usize) -> Option { + let item = &self.items[index]; + self.find(index, true, |p| p.level < item.level) + } + + // rev start: start - 1 + fn find(&self, start: usize, rev: bool, f: F) -> Option + where + F: FnMut(&Elem) -> bool, + { + let iter = self.items.iter(); + if rev { + iter.take(start).rposition(f) + } else { + iter.skip(start).position(f).map(|p| p + start) + } + } +} + +impl Tree { + pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params) { + if self.items.is_empty() { + return; + } + if let Some(next_level) = self.next_item().map(|elem| elem.level) { + let current = &mut self.items[self.selected]; + let current_level = current.level; + if next_level > current_level { + if let Some(mut on_folded_fn) = self.on_folded_fn.take() { + on_folded_fn(&mut current.item, cx, params); + self.on_folded_fn = Some(on_folded_fn); + } + self.fold_current_child(); + return; + } + } + + if let Some(mut on_open_fn) = self.on_opened_fn.take() { + let mut f = || { + let current = &mut self.items[self.selected]; + let items = match on_open_fn(&mut current.item, cx, params) { + TreeOp::Restore => { + let inserts = std::mem::take(&mut current.folded); + let _: Vec<_> = self + .items + .splice(self.selected + 1..self.selected + 1, inserts) + .collect(); + return; + } + TreeOp::InsertChild(items) => items, + TreeOp::GetChildsAndInsert => match current.item.get_childs() { + Ok(items) => items, + Err(e) => return cx.editor.set_error(format!("{e}")), + }, + TreeOp::ReplaceTree(items) => return self.replace_with_new_items(items), + TreeOp::Noop => return, + }; + current.folded = vec![]; + let inserts = vec_to_tree(items, current.level + 1); + let _: Vec<_> = self + .items + .splice(self.selected + 1..self.selected + 1, inserts) + .collect(); + }; + f(); + self.on_opened_fn = Some(on_open_fn) + } else { + let current = &mut self.items[self.selected]; + let inserts = std::mem::take(&mut current.folded); + let _: Vec<_> = self + .items + .splice(self.selected + 1..self.selected + 1, inserts) + .collect(); + } + } + + pub fn fold_current_level(&mut self) { + let start = match self.find_parent(self.selected) { + Some(start) => start, + None => return, + }; + self.selected = start; + self.fold_current_child(); + } + + pub fn fold_current_child(&mut self) { + if self.selected + 1 >= self.items.len() { + return; + } + let pos = self.next_not_descendant_pos(self.selected); + if self.selected < pos { + self.items[self.selected].folded = self.items.drain(self.selected + 1..pos).collect(); + } + } + + pub fn search_next(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) { + let skip = self.save_view.0 + 1; + self.selected = self + .find(skip, false, |e| e.item.filter(cx, s, params)) + .unwrap_or(self.save_view.0); + + self.row = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); + } + + pub fn search_pre(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) { + let take = self.save_view.0; + self.selected = self + .find(take, true, |e| e.item.filter(cx, s, params)) + .unwrap_or(self.save_view.0); + + self.row = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); + } + + pub fn move_down(&mut self, rows: usize) { + let len = self.items.len(); + if len > 0 { + self.selected = std::cmp::min(self.selected + rows, len.saturating_sub(1)); + self.row = std::cmp::min(self.selected, self.row + rows); + } + } + + pub fn move_up(&mut self, rows: usize) { + let len = self.items.len(); + if len > 0 { + self.selected = self.selected.saturating_sub(rows); + self.row = std::cmp::min(self.selected, self.row.saturating_sub(rows)); + } + } + + pub fn move_left(&mut self, cols: usize) { + self.col = self.col.saturating_sub(cols); + } + + pub fn move_right(&mut self, cols: usize) { + self.pre_render = Some(Box::new(move |tree: &mut Self, area: Rect| { + let max_scroll = tree.max_len.saturating_sub(area.width as usize); + tree.col = max_scroll.min(tree.col + cols); + })); + } + + pub fn move_down_half_page(&mut self) { + self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { + tree.move_down((area.height / 2) as usize); + })); + } + + pub fn move_up_half_page(&mut self) { + self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { + tree.move_up((area.height / 2) as usize); + })); + } + + pub fn move_down_page(&mut self) { + self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { + tree.move_down((area.height) as usize); + })); + } + + pub fn move_up_page(&mut self) { + self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { + tree.move_up((area.height) as usize); + })); + } + + pub fn save_view(&mut self) { + self.save_view = (self.selected, self.row); + } + + pub fn restore_view(&mut self) { + (self.selected, self.row) = self.save_view; + } + + pub fn current(&self) -> &Elem { + &self.items[self.selected] + } + + pub fn current_item(&self) -> &T { + &self.items[self.selected].item + } + + pub fn row(&self) -> usize { + self.row + } + + pub fn remove_current(&mut self) -> T { + let elem = self.items.remove(self.selected); + self.selected = self.selected.saturating_sub(1); + elem.item + } + + pub fn replace_current(&mut self, item: T) { + self.items[self.selected].item = item; + } + + pub fn select(&mut self, select_item: &T) { + let selected = self + .items + .iter() + .enumerate() + .filter(|(_, i)| i.item.cmp(select_item) == Ordering::Equal) + .next(); + if let Some((idx, _)) = selected { + self.selected = idx; + self.row = idx; + } + } + + pub fn insert_current_level(&mut self, item: T) { + let current = self.current(); + let level = current.level; + let pos = match current.item.cmp(&item) { + Ordering::Less => self + .find(self.selected + 1, false, |e| { + e.level < level || (e.level == level && e.item.cmp(&item) != Ordering::Less) + }) + .unwrap_or(self.items.len()), + + Ordering::Greater => { + match self.find(self.selected, true, |elem| { + elem.level < level + || (elem.level == level && elem.item.cmp(&item) != Ordering::Greater) + }) { + Some(p) if self.items[p].level == level => self.next_not_descendant_pos(p), + Some(p) => p + 1, + None => 0, + } + } + Ordering::Equal => self.selected + 1, + }; + self.items.insert(pos, Elem::new(item, level)); + } +} + +impl Tree { + pub fn render( + &mut self, + area: Rect, + surface: &mut Surface, + cx: &mut Context, + params: &mut T::Params, + ) { + if let Some(pre_render) = self.pre_render.take() { + pre_render(self, area); + } + + self.max_len = 0; + self.row = std::cmp::min(self.row, area.height.saturating_sub(1) as usize); + let style = cx.editor.theme.get(&self.tree_symbol_style); + let folder_style = cx.editor.theme.get("special"); + let last_item_index = self.items.len().saturating_sub(1); + let skip = self.selected.saturating_sub(self.row); + let iter = self + .items + .iter() + .skip(skip) + .take(area.height as usize) + .enumerate(); + for (index, elem) in iter { + let row = index as u16; + let mut area = Rect::new(area.x, area.y + row, area.width, 1); + let indent = if elem.level > 0 { + if index + skip != last_item_index { + format!("{}", "│ ".repeat(elem.level - 1)) + } else { + format!("{}", "".repeat(elem.level - 1)) + } + } else { + "".to_string() + }; + + let indent_len = indent.chars().count(); + if indent_len > self.col { + let indent: String = indent.chars().skip(self.col).collect(); + if !indent.is_empty() { + surface.set_stringn(area.x, area.y, &indent, area.width as usize, style); + area = area.clip_left(indent.width() as u16); + } + }; + let mut start_index = self.col.saturating_sub(indent_len); + let mut text = elem.item.text(cx, skip + index == self.selected, params); + self.max_len = self.max_len.max(text.width() + indent.len() - 2); + for span in text.0.iter_mut() { + if area.width == 0 { + return; + } + if start_index == 0 { + let mut icon_offset = 0; + if let Some((icon, color)) = elem.item.icon() { + let style = folder_style.fg(*color); + surface.set_string(area.x, area.y, icon, style); + icon_offset = 2; + } + surface.set_span(area.x + icon_offset, area.y, span, area.width - icon_offset); + area = area.clip_left((span.width() - icon_offset as usize) as u16); + } else { + let span_width = span.width(); + if start_index > span_width { + start_index -= span_width; + } else { + let content: String = span + .content + .chars() + .filter(|c| { + if start_index > 0 { + start_index = start_index.saturating_sub(c.to_string().width()); + false + } else { + true + } + }) + .collect(); + let mut cont = String::new(); + cont.push_str(""); + cont.push_str(&content); + surface.set_string_truncated( + area.x, + area.y, + &cont, + area.width as usize, + |_| span.style, + false, + false, + ); + start_index = 0 + } + } + } + } + } + + pub fn handle_event( + &mut self, + event: Event, + cx: &mut Context, + params: &mut T::Params, + ) -> EventResult { + let key_event = match event { + Event::Key(event) => event, + Event::Resize(..) => return EventResult::Consumed(None), + _ => return EventResult::Ignored(None), + }; + if let Some(mut on_next_key) = self.on_next_key.take() { + on_next_key(cx, self, key_event); + return EventResult::Consumed(None); + } + let count = std::mem::replace(&mut self.count, 0); + match key_event.into() { + key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10, + key!('k') | shift!(Tab) | key!(Up) => self.move_up(1.max(count)), + key!('j') | key!(Tab) | key!(Down) => self.move_down(1.max(count)), + key!('z') => self.fold_current_level(), + key!('h') => self.move_left(1.max(count)), + key!('l') => self.move_right(1.max(count)), + shift!('G') => self.move_down(usize::MAX / 2), + key!(Enter) => self.on_enter(cx, params), + key!(' ') => self.on_enter(cx, params), + ctrl!('d') | ctrl!('j') => self.move_down_half_page(), + ctrl!('u') | ctrl!('k') => self.move_up_half_page(), + shift!('D') => self.move_down_page(), + shift!('U') => self.move_up_page(), + key!('g') => { + self.on_next_key = Some(Box::new(|_, tree, event| match event.into() { + key!('g') => tree.move_up(usize::MAX / 2), + key!('e') => tree.move_down(usize::MAX / 2), + _ => {} + })); + } + _ => return EventResult::Ignored(None), + } + + EventResult::Consumed(None) + } +} + +impl Tree { + pub fn filter(&mut self, s: &str, cx: &mut Context, params: &mut T::Params) { + fn filter_recursion( + elems: &Vec>, + mut index: usize, + s: &str, + cx: &mut Context, + params: &mut T::Params, + ) -> (Vec>, usize) + where + T: TreeItem + Clone, + { + let mut retain = vec![]; + let elem = &elems[index]; + loop { + let child = match elems.get(index + 1) { + Some(child) if child.item.is_child(&elem.item) => child, + _ => break, + }; + index += 1; + let next = elems.get(index + 1); + if next.map_or(false, |n| n.item.is_child(&child.item)) { + let (sub_retain, current_index) = filter_recursion(elems, index, s, cx, params); + retain.extend(sub_retain); + index = current_index; + } else if child.item.filter(cx, s, params) { + retain.push(child.clone()); + } + } + if !retain.is_empty() || elem.item.filter(cx, s, params) { + retain.insert(0, elem.clone()); + } + (retain, index) + } + + if s.is_empty() { + if let Some((_, recycle)) = self.recycle.take() { + self.items = recycle; + self.restore_view(); + return; + } + } + + let mut retain = vec![]; + let mut index = 0; + let items = match &self.recycle { + Some((pre, _)) if pre == s => return, + Some((pre, recycle)) if pre.contains(s) => recycle, + _ => &self.items, + }; + while let Some(elem) = items.get(index) { + let next = items.get(index + 1); + if next.map_or(false, |n| n.item.is_child(&elem.item)) { + let (sub_items, current_index) = filter_recursion(items, index, s, cx, params); + index = current_index; + retain.extend(sub_items); + } else if elem.item.filter(cx, s, params) { + retain.push(elem.clone()) + } + index += 1; + } + + if retain.is_empty() { + if let Some((_, recycle)) = self.recycle.take() { + self.items = recycle; + self.restore_view(); + } + return; + } + + let recycle = std::mem::replace(&mut self.items, retain); + if let Some(r) = self.recycle.as_mut() { + r.0 = s.into() + } else { + self.recycle = Some((s.into(), recycle)); + self.save_view(); + } + + self.selected = self + .find(0, false, |elem| elem.item.filter(cx, s, params)) + .unwrap_or(0); + self.row = self.selected; + } + + pub fn clean_recycle(&mut self) { + self.recycle = None; + } + + pub fn restore_recycle(&mut self) { + if let Some((_, recycle)) = self.recycle.take() { + self.items = recycle; + } + } +} diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index a4a1c389..648af4c1 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -16,11 +16,11 @@ include = ["src/**/*", "README.md"] default = ["crossterm"] [dependencies] -bitflags = "1.3" -cassowary = "0.3" -unicode-segmentation = "1.10" -crossterm = { version = "0.25", optional = true } -termini = "0.1" -serde = { version = "1", "optional" = true, features = ["derive"]} +bitflags = "1.3.2" +cassowary = "0.3.0" +unicode-segmentation = "1.10.0" +crossterm = { version = "0.25.0", optional = true } +termini = "0.1.4" +serde = { version = "1.0.152", "optional" = true, features = ["derive"] } helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-core = { version = "0.6", path = "../helix-core" } diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 19b660a6..d8c033cb 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -13,16 +13,16 @@ homepage = "https://helix-editor.com" [dependencies] helix-core = { version = "0.6", path = "../helix-core" } -tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } -parking_lot = "0.12" +tokio = { version = "1.24.1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } +parking_lot = "0.12.1" -git-repository = { version = "0.32", default-features = false , optional = true } +git-repository = { version = "0.33.0", default-features = false, optional = true } imara-diff = "0.1.5" -log = "0.4" +log = "0.4.17" [features] git = ["git-repository"] [dev-dependencies] -tempfile = "3.3" \ No newline at end of file +tempfile = "3.3.0" diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index e7a20496..82ea05df 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -14,39 +14,39 @@ default = [] term = ["crossterm"] [dependencies] -bitflags = "1.3" -anyhow = "1" +bitflags = "1.3.2" +anyhow = "1.0.68" helix-core = { version = "0.6", path = "../helix-core" } helix-loader = { version = "0.6", path = "../helix-loader" } helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-dap = { version = "0.6", path = "../helix-dap" } -crossterm = { version = "0.25", optional = true } +crossterm = { version = "0.25.0", optional = true } helix-vcs = { version = "0.6", path = "../helix-vcs" } # Conversion traits -once_cell = "1.17" -url = "2" +once_cell = "1.17.0" +url = "2.3.1" -arc-swap = { version = "1.6.0" } +arc-swap = "1.6.0" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } -tokio-stream = "0.1" -futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } +tokio = { version = "1.24.1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } +tokio-stream = "0.1.11" +futures-util = { version = "0.3.25", features = ["std", "async-await"], default-features = false } -slotmap = "1" +slotmap = "1.0.6" -chardetng = "0.1" +chardetng = "0.1.17" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.5" -log = "~0.4" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +toml = "0.5.10" +log = "0.4.17" -which = "4.2" +which = "4.3.0" [target.'cfg(windows)'.dependencies] -clipboard-win = { version = "4.5", features = ["std"] } +clipboard-win = { version = "4.5.0", features = ["std"] } [dev-dependencies] helix-tui = { path = "../helix-tui" } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 856e5628..af948ecd 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -128,7 +128,7 @@ pub struct Document { // be more troublesome. pub history: Cell, - pub savepoint: Option, + pub savepoint: Option<(i32, Transaction)>, last_saved_revision: usize, version: i32, // should be usize? @@ -510,6 +510,21 @@ impl Document { Some(fut.boxed()) } + /// Deletes the file associated with this document + pub fn delete(&mut self) -> impl Future> { + let path = self + .path() + .expect("Cannot delete with no path set!") + .clone(); + + async move { + use tokio::fs; + fs::remove_file(path).await?; + + Ok(()) + } + } + pub fn save>( &mut self, path: Option

, @@ -811,7 +826,8 @@ impl Document { if self.savepoint.is_some() { take_with(&mut self.savepoint, |prev_revert| { let revert = transaction.invert(&old_doc); - Some(revert.compose(prev_revert.unwrap())) + let (version, prev_revert) = prev_revert.unwrap(); + Some((version, revert.compose(prev_revert))) }); } @@ -906,11 +922,11 @@ impl Document { } pub fn savepoint(&mut self) { - self.savepoint = Some(Transaction::new(self.text())); + self.savepoint = Some((self.version, Transaction::new(self.text()))); } pub fn restore(&mut self, view: &mut View) { - if let Some(revert) = self.savepoint.take() { + if let Some((_, revert)) = self.savepoint.take() { apply_transaction(&revert, self, view); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 547a4ffb..b5f4b34a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -115,6 +115,57 @@ impl Default for FilePickerConfig { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ExplorerStyle { + Tree, + List, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ExplorerPosition { + Embed, + Overlay, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct ExplorerConfig { + pub style: ExplorerStyle, + pub position: ExplorerPosition, + /// explorer column width + pub column_width: usize, +} + +impl ExplorerConfig { + pub fn is_embed(&self) -> bool { + return self.position == ExplorerPosition::Embed; + } + + pub fn is_overlay(&self) -> bool { + return self.position == ExplorerPosition::Overlay; + } + + pub fn is_list(&self) -> bool { + return self.style == ExplorerStyle::List; + } + + pub fn is_tree(&self) -> bool { + return self.style == ExplorerStyle::Tree; + } +} + +impl Default for ExplorerConfig { + fn default() -> Self { + Self { + style: ExplorerStyle::Tree, + position: ExplorerPosition::Embed, + column_width: 30, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { @@ -154,6 +205,7 @@ pub struct Config { )] pub idle_timeout: Duration, pub completion_trigger_len: u8, + pub completion_trigger_chars: Vec, /// Whether to display infoboxes. Defaults to true. pub auto_info: bool, pub file_picker: FilePickerConfig, @@ -178,6 +230,8 @@ pub struct Config { pub indent_guides: IndentGuidesConfig, /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, + /// explore config + pub explorer: ExplorerConfig, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -280,7 +334,16 @@ impl Default for StatusLineConfig { Self { left: vec![E::Mode, E::Spinner, E::FileName], center: vec![], - right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding], + right: vec![ + E::Diagnostics, + E::Selections, + E::Position, + E::PositionPercentage, + E::Separator, + E::FileEncoding, + E::FileLineEnding, + E::FileType, + ], separator: String::from("│"), mode: ModeConfig::default(), } @@ -407,7 +470,7 @@ impl std::ops::Deref for CursorShapeConfig { impl Default for CursorShapeConfig { fn default() -> Self { - Self([CursorKind::Block; 3]) + Self([CursorKind::Block, CursorKind::Underline, CursorKind::Bar]) } } @@ -574,20 +637,30 @@ impl Default for WhitespaceCharacters { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum RainbowIndentOptions { + None, + Dim, + Normal, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] pub struct IndentGuidesConfig { pub render: bool, pub character: char, + pub rainbow: RainbowIndentOptions, pub skip_levels: u8, } impl Default for IndentGuidesConfig { fn default() -> Self { Self { + render: true, skip_levels: 0, - render: false, character: '│', + rainbow: RainbowIndentOptions::None, } } } @@ -604,7 +677,7 @@ impl Default for Config { vec!["sh".to_owned(), "-c".to_owned()] }, line_number: LineNumber::Absolute, - cursorline: false, + cursorline: true, cursorcolumn: false, gutters: vec![ GutterType::Diagnostics, @@ -628,11 +701,13 @@ impl Default for Config { search: SearchConfig::default(), lsp: LspConfig::default(), terminal: get_terminal_provider(), - rulers: Vec::new(), + rulers: vec![120], whitespace: WhitespaceConfig::default(), bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), - color_modes: false, + color_modes: true, + explorer: ExplorerConfig::default(), + completion_trigger_chars: vec!['.'], } } } @@ -768,6 +843,7 @@ pub enum Action { } /// Error thrown on failed document closed +#[derive(Debug)] pub enum CloseError { /// Document doesn't exist DoesNotExist, @@ -777,6 +853,18 @@ pub enum CloseError { SaveError(anyhow::Error), } +impl std::error::Error for CloseError {} + +impl std::fmt::Display for CloseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CloseError::DoesNotExist => "Buffer does not exist".fmt(f), + CloseError::BufferModified(s) => write!(f, "The buffer {s} has been modified"), + CloseError::SaveError(e) => write!(f, "Failed to save document {e}"), + } + } +} + impl Editor { pub fn new( mut area: Rect, @@ -860,6 +948,10 @@ impl Editor { .reset(Instant::now() + config.idle_timeout); } + pub fn reset_idle_timer_zero(&mut self) { + self.idle_timer.as_mut().reset(Instant::now()); + } + pub fn clear_status(&mut self) { self.status_msg = None; } @@ -938,6 +1030,27 @@ impl Editor { Self::launch_language_server(&mut self.language_servers, doc) } + /// Restarts a language server for a given document + pub fn restart_language_server(&mut self, doc_id: DocumentId) -> Option<()> { + let doc = self.documents.get_mut(&doc_id)?; + if let Some(language) = doc.language.as_ref() { + if let Ok(client) = self + .language_servers + .restart(&*language, doc.path()) + .map_err(|e| { + log::error!( + "Failed to restart the LSP for `{}` {{ {} }}", + language.scope(), + e + ) + }) + { + doc.set_language_server(client); + } + }; + Some(()) + } + /// Launch a language server for a given document fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> { // if doc doesn't have a URL it's a scratch buffer, ignore it diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index cb0d3ac4..662c5a5b 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -202,6 +202,7 @@ pub struct Theme { styles: HashMap, // tree-sitter highlight styles are stored in a Vec to optimize lookups scopes: Vec, + rainbow_length: usize, highlights: Vec