Compare commits

...

77 Commits

Author SHA1 Message Date
Blaž Hrastnik
7e8603247d Merge pull request #66 from IceDragon200/replaced-args-parser
Drop pico-args in favour of a hand rolled parser
2021-06-03 10:32:42 +09:00
Blaž Hrastnik
7140908f6e Nix: add lldb to shell 2021-06-03 10:31:33 +09:00
Blaž Hrastnik
6dba1e7ec7 Clippy lint 2021-06-03 10:31:14 +09:00
Blaž Hrastnik
c0332bd935 Fix split sizes getting out of sync with the terminal size, refs #69 2021-06-03 10:28:49 +09:00
Blaž Hrastnik
3c7729906c Merge pull request #70 from RLHerbert/master
Fix panic when buffer larger than terminal width
2021-06-03 10:28:14 +09:00
Rowan Herbert
1b67fae9f4 Fix panic when buffer larger than terminal width 2021-06-02 16:30:40 -07:00
Corey Powell
f0018280cb Refactored parse_args loop
Thanks @PabloMansanet
2021-06-02 14:26:20 -05:00
Corey Powell
7202953e69 Dropped pico-args in favour of a simpler hand roller parser
Not the greatest looking, but it gets the job done
2021-06-02 14:26:13 -05:00
Corey Powell
7761c88d61 Merge pull request #62 from pickfire/cell
Separate document history into Cell
2021-06-02 13:27:35 -05:00
Corey Powell
68f5031dcc Merge pull request #49 from eleijonmarck/patch-1
Update README.md to include shortcuts
2021-06-02 13:15:32 -05:00
Corey Powell
83031564db Merge pull request #57 from pickfire/fix-panic
Fix panic opening rust file
2021-06-02 13:14:19 -05:00
Ivan Tham
eab6e53511 Fix panic opening rust file
Application::new will use stuff that requires tokio runtime.
2021-06-02 23:49:26 +08:00
Ivan Tham
f5f46b1fed Separate document history into Cell
As history is used separately from the rest of the edits, separating it
can avoid needless borrowing and cloning. But one need to be aware later.
2021-06-02 23:47:50 +08:00
Eric Leijonmarck
5f49bafbe8 Update README.md 2021-06-02 17:05:15 +02:00
Blaž Hrastnik
2719a35123 Merge pull request #55 from helix-editor/autoresize
autoresize terminal in compositor render
2021-06-02 22:45:43 +09:00
Blaž Hrastnik
0a6672c626 Merge pull request #50 from wojciechkepka/config
Use config_dir for logging, create config_dir
2021-06-02 22:43:28 +09:00
Blaž Hrastnik
b51111a364 Merge pull request #21 from IceDragon200/elixir-syntax
Added elixir syntax
2021-06-02 22:41:51 +09:00
Jan Hrastnik
78980f575b autoresize terminal in compositor render 2021-06-02 15:40:08 +02:00
Corey Powell
0bb375bafa Added missing tree-sitter-elixir submodule 2021-06-02 06:43:22 -05:00
Eric Leijonmarck
c960bcfc24 Update README.md 2021-06-02 13:15:31 +02:00
Wojciech Kępka
e88383d990 Use config_dir for logging, create config_dir 2021-06-02 12:25:25 +02:00
Eric Leijonmarck
312b29f712 Update README.md 2021-06-02 12:05:39 +02:00
Blaž Hrastnik
f4560cb68a Better fix for w/e that also covers ia<esc>we/ia<esc>wb 2021-06-02 14:57:43 +09:00
Blaž Hrastnik
cbb3ebafdc Support ctrl-f and ctrl-b to page up/down, fixes #41 2021-06-02 13:20:36 +09:00
Blaž Hrastnik
0851110d10 f/t: Check if at bounds before searching, refs #43, closes #37 2021-06-02 13:20:27 +09:00
Blaž Hrastnik
3ace581191 Fix panics when triggering w or e on the last char of the line
Closes #32
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
c0264b9f7f fix: Don't allow moving past last line, fixes #30, #24
Off by 1 error
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
22dad592b8 Merge pull request #40 from data0x200/fix-empty-command
Fix empty command cause panic
2021-06-02 13:06:57 +09:00
Corey Powell
ca042a4bde Added elixir syntax
Using custom fork for now to get around generating the source files
2021-06-01 21:59:16 -05:00
Blaž Hrastnik
67b1cd32c7 Update install notes 2021-06-02 11:14:46 +09:00
Daichi Takamiya
4d12c7c3cf Fix empty command cause panic 2021-06-02 10:55:32 +09:00
Blaž Hrastnik
4f56a8e248 book: Always generate the CNAME file 2021-06-02 10:24:00 +09:00
Blaž Hrastnik
dbc392d92c Run fmt 2021-06-02 09:56:50 +09:00
Blaž Hrastnik
7967d312c0 Merge pull request #38 from nathom/master
Add .DS_Store to ignored directories
2021-06-02 09:37:09 +09:00
Blaž Hrastnik
db48d22384 Merge pull request #19 from wojciechkepka/archinstall
Add Arch Linux installation instructions to README
2021-06-02 09:30:41 +09:00
Blaž Hrastnik
533ff61d0e Merge pull request #34 from DanySpin97/improve-error
Improve errors handling in main by adding context
2021-06-02 09:30:16 +09:00
nathom
b1ce969d80 Add .DS_Store to ignored directories 2021-06-01 17:29:37 -07:00
Blaž Hrastnik
01bf363446 Merge pull request #31 from wullewutz/patch-1
Fixed c/p error in keymap doc
2021-06-02 09:26:29 +09:00
Blaž Hrastnik
cc323f7665 Merge pull request #36 from swdunlop/patch-1
Make HELIX_RUNTIME depend on pwd, not speed's HOME
2021-06-02 09:26:04 +09:00
Scott Dunlop
60caaf7fc4 Make HELIX_RUNTIME depend on pwd, not speed's HOME 2021-06-01 15:03:57 -07:00
Danilo Spinella
ea824ed05d Improve errors handling in main by adding context
Return a anyhow::Result in main function so that Context can be used
there too.
2021-06-01 23:27:16 +02:00
wullewutz
cfae07e7ba Fixed c/p error in keymap doc
Go to definition mapping is "gd" not "ge"
2021-06-01 22:36:42 +02:00
wojciechkepka
56dbc60840 Add Arch Linux installation instructions to README 2021-06-01 21:08:09 +02:00
Blaž Hrastnik
c2e6b9f506 Add typescript support & ts/js indentation queries 2021-06-01 17:55:11 +09:00
Blaž Hrastnik
8fd8006043 Golang indent queries 2021-06-01 17:26:10 +09:00
Blaž Hrastnik
ce25aa951e Allow setting a filepath on :write 2021-06-01 17:26:03 +09:00
Blaž Hrastnik
a2147fc7d5 Change help prompt styling 2021-06-01 12:00:25 +09:00
Blaž Hrastnik
d8e16554bf Don't crash if no filename specified on open 2021-06-01 11:59:59 +09:00
Blaž Hrastnik
2cc30cd07c Categorize _ as a word char, not punctuation 2021-05-31 21:09:17 +09:00
Blaž Hrastnik
0dde5f2cae Fix badge 2021-05-31 21:09:07 +09:00
Blaž Hrastnik
b8d6e6ad28 Allow setting verbosity to info again 2021-05-31 17:14:49 +09:00
Blaž Hrastnik
5825bce0e4 Link to install/keymap docs, fix build status badge 2021-05-31 17:12:09 +09:00
Blaž Hrastnik
aeabfc55a8 Adjust golang indents yet again 2021-05-31 17:09:19 +09:00
Blaž Hrastnik
17e9386388 Allow moving to EOL byte, also fixes #15 2021-05-31 17:08:19 +09:00
Blaž Hrastnik
138787f76e Drop clap for pico-args
We barely have any flags so it's not worth the compilation time or
binary size to use clap.
2021-05-31 17:07:43 +09:00
Blaž Hrastnik
1132c5122f Update mdbook styling, add link to AUR 2021-05-31 00:32:21 +09:00
Blaž Hrastnik
4a8053e832 Merge pull request #13 from helix-editor/dependabot/cargo/tokio-1.6.1
Bump tokio from 1.6.0 to 1.6.1
2021-05-30 18:02:52 +09:00
Blaž Hrastnik
e033a4b8ac Merge pull request #11 from helix-editor/dependabot/github_actions/actions/upload-artifact-2.2.3
Bump actions/upload-artifact from 1 to 2.2.3
2021-05-30 17:58:43 +09:00
Blaž Hrastnik
acbcd758bd Merge pull request #12 from helix-editor/dependabot/github_actions/actions/cache-2.1.6
Bump actions/cache from 1 to 2.1.6
2021-05-30 17:58:21 +09:00
dependabot[bot]
76eed4caad Bump tokio from 1.6.0 to 1.6.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.6.0...tokio-1.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:43 +00:00
dependabot[bot]
3170c49be8 Bump actions/cache from 1 to 2.1.6
Bumps [actions/cache](https://github.com/actions/cache) from 1 to 2.1.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v1...v2.1.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:19 +00:00
dependabot[bot]
0327d66653 Bump actions/upload-artifact from 1 to 2.2.3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 2.2.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v2.2.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:15 +00:00
Blaž Hrastnik
c67e31830d Add dependabot config 2021-05-30 17:52:56 +09:00
Blaž Hrastnik
6460501a44 Update architecture.md 2021-05-30 17:52:46 +09:00
Blaž Hrastnik
67b037050f Adjust rust indents 2021-05-30 17:13:32 +09:00
Blaž Hrastnik
87d0617f3b Completion: Format docs tabs & highlight in the doc's native language 2021-05-30 17:13:02 +09:00
Blaž Hrastnik
668f735232 Update the README's matrix link 2021-05-30 17:12:43 +09:00
Blaž Hrastnik
a3a9502596 Add a github pages auto-build action. 2021-05-30 17:12:29 +09:00
Blaž Hrastnik
3810650a6b Completion: Render non-markdown docs too 2021-05-30 10:36:58 +09:00
Blaž Hrastnik
2c48d65b15 Format document on save 2021-05-30 00:00:15 +09:00
Blaž Hrastnik
d5466eddf5 Update flake deps 2021-05-29 23:59:30 +09:00
Blaž Hrastnik
d54ae09d3b ESC should exit both completion and insert mode 2021-05-29 10:37:47 +09:00
Blaž Hrastnik
a28eaa81a0 Golang indent adjustment 2021-05-29 00:06:38 +09:00
Blaž Hrastnik
d708efe275 Fix cursor positioning for prompts 2021-05-29 00:06:23 +09:00
Blaž Hrastnik
3336023614 ui: Menu rendering adjustments 2021-05-28 00:01:17 +09:00
Blaž Hrastnik
094203c74e Update deps, introduce the new tree-sitter lifetimes 2021-05-28 00:00:51 +09:00
Blaž Hrastnik
b114cfa119 Display more data in completion popups. 2021-05-22 17:33:42 +09:00
55 changed files with 1082 additions and 458 deletions

14
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -25,19 +25,19 @@ jobs:
override: true
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -64,19 +64,19 @@ jobs:
override: true
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -104,19 +104,19 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

27
.github/workflows/gh-pages.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Github Pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: 'latest'
# mdbook-version: '0.4.8'
- run: mdbook build book
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/master'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book

View File

@@ -96,7 +96,7 @@ jobs:
cp "target/${{ matrix.target }}/release/hx" "dist/"
fi
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v2.2.3
with:
name: bins-${{ matrix.build }}
path: dist

4
.gitmodules vendored
View File

@@ -82,3 +82,7 @@
path = helix-syntax/languages/tree-sitter-toml
url = https://github.com/ikatyang/tree-sitter-toml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/IceDragon200/tree-sitter-elixir
shallow = true

90
Cargo.lock generated
View File

@@ -52,9 +52,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.67"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
dependencies = [
"jobserver",
]
@@ -77,21 +77,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
dependencies = [
"bitflags",
"indexmap",
"lazy_static",
"os_str_bytes",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.4"
@@ -244,9 +229,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
@@ -272,17 +257,10 @@ dependencies = [
"regex",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "helix-core"
version = "0.1.0"
dependencies = [
"anyhow",
"etcetera",
"helix-syntax",
"once_cell",
@@ -335,7 +313,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"crossterm",
"dirs-next",
"fern",
@@ -423,16 +400,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indexmap"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.9"
@@ -477,9 +444,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
[[package]]
name = "lock_api"
@@ -501,9 +468,9 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.89.0"
version = "0.89.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07731ecd4ee0654728359a5b95e2a254c857876c04b85225496a35d60345daa7"
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f"
dependencies = [
"bitflags",
"serde",
@@ -602,12 +569,6 @@ version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "os_str_bytes"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "parking_lot"
version = "0.11.1"
@@ -659,9 +620,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
"unicode-xid",
]
@@ -857,29 +818,20 @@ dependencies = [
"utf-8",
]
[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
dependencies = [
"proc-macro2",
"quote",
@@ -921,9 +873,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.6.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37"
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
dependencies = [
"autocfg",
"bytes",
@@ -972,9 +924,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.19.3"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f41201fed3db3b520405a9c01c61773a250d4c3f43e9861c14b2bb232c981ab"
checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0"
dependencies = [
"cc",
"regex",
@@ -1044,12 +996,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"

View File

@@ -1,7 +1,7 @@
# Helix
[![Build status](https://github.com/helix-editor/helix/workflows/ci/badge.svg)](https://github.com/helix-editor/helix/actions)
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
![Screenshot](./screenshot.png)
@@ -10,7 +10,8 @@ A kakoune / neovim inspired editor, written in Rust.
The editing model is very heavily based on kakoune; during development I found
myself agreeing with most of kakoune's design decisions.
For more information, see the [website](https://helix-editor.com).
For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).
# Features
@@ -41,6 +42,14 @@ Now copy the `runtime/` directory somewhere. Helix will by default look for the
runtime inside the same folder as the executable, but that can be overriden via
the `HELIX_RUNTIME` environment variable.
> NOTE: You should set this to <path to repository>/runtime in development (if
> running via cargo).
## Arch Linux
There are two packages available from AUR:
- `helix-bin`: contains prebuilt binary from GitHub releases
- `helix-git`: builds the master branch of this repository
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
@@ -56,7 +65,14 @@ Some suggestions to get started:
We provide an [architecture.md](./docs/architecture.md) that should give you
a good overview of the internals.
## Usage
### Keyboard shortcuts / Keymap
All shortcuts/keymaps can be found in the documentation on the website:
- https://docs.helix-editor.com/keymap.html
# Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-editor:matrix.org).
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-community:matrix.org).

View File

@@ -2,6 +2,8 @@
------
as you type completion!
- tree sitter:
- lua
- markdown
@@ -18,6 +20,9 @@
- [ ] document.on_type provider triggers
- [ ] completion isIncomplete support
- [ ] scroll wheel support
- [ ] matching bracket highlight
1
- [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range)

View File

@@ -4,3 +4,6 @@ language = "en"
multilingual = false
src = "src"
theme = "colibri"
[output.html]
cname = "docs.helix-editor.com"

View File

@@ -6,10 +6,7 @@ We provide pre-built binaries on the [GitHub Releases page](https://github.com/h
TODO: brew tap
```
$ brew tap helix-editor/helix
$ brew install helix
```
Please use a pre-built binary release for the time being.
## Linux
@@ -21,7 +18,9 @@ shell for working on Helix.
### Arch Linux
TODO: AUR
Binary packages are available on AUR:
- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
## Build from source

View File

@@ -118,7 +118,7 @@ Jumps to various locations.
|-----|-----------|
| g | Go to the start of the file |
| e | Go to the end of the file |
| e | Go to definition |
| d | Go to definition |
| t | Go to type definition |
| r | Go to references |
| i | Go to implementation |

View File

@@ -109,7 +109,7 @@ h6:target::before {
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p, .content ol, .content ul, .content table, .content blockquote {
.content p, .content ol, .content ul, .content table {
margin-top: 0;
margin-bottom: .875em;
}
@@ -123,8 +123,7 @@ h6:target::before {
.content .header:link,
.content .header:visited {
color: var(--fg);
/* color: white; */
color: #281733;
color: var(--heading-fg);
}
.content .header:link,
.content .header:visited:hover {
@@ -168,12 +167,15 @@ table tbody tr:nth-child(2n) {
blockquote {
margin: 20px 0;
padding: 0 20px;
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-top: .1em solid var(--quote-border);
border-bottom: .1em solid var(--quote-border);
border-left: 4px solid var(--quote-border);
}
blockquote *:last-child {
margin-bottom: 0;
}

View File

@@ -13,6 +13,7 @@
.ayu {
--bg: hsl(210, 25%, 8%);
--fg: #c5c5c5;
--heading-fg: #c5c5c5;
--sidebar-bg: #14191f;
--sidebar-fg: #c8c9db;
@@ -53,6 +54,7 @@
.coal {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--heading-fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
@@ -93,6 +95,7 @@
.light {
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%);
--heading-fg: hsl(0, 0%, 0%);
--sidebar-bg: #fafafa;
--sidebar-fg: hsl(0, 0%, 0%);
@@ -133,6 +136,7 @@
.navy {
--bg: hsl(226, 23%, 11%);
--fg: #bcbdd0;
--heading-fg: #bcbdd0;
--sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db;
@@ -173,6 +177,7 @@
.rust {
--bg: hsl(60, 9%, 87%);
--fg: #262625;
--heading-fg: #262625;
--sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db;
@@ -214,6 +219,7 @@
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #ebeafa;
--heading-fg: #ebeafa;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
@@ -297,6 +303,7 @@
--bg: #ffffff;
--fg: #452859;
--fg: #5a5977;
--heading-fg: #281733;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
@@ -317,8 +324,8 @@
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--quote-bg: rgba(0, 0, 0, 0);
--quote-border: hsl(226, 15%, 75%);
--table-border-color: #5a5977;
--table-border-color: hsl(201deg 10% 67%);

View File

@@ -8,68 +8,78 @@
| helix-term | Terminal UI |
| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
# Notes
- server-client architecture via gRPC, UI separate from core
- multi cursor based editing and slicing
- WASM based plugins (builtin LSP & fuzzy file finder)
This document contains a high-level overview of Helix internals.
Structure similar to codemirror:
> NOTE: Use `cargo doc --open` for API documentation as well as dependency
> documentation.
- text (ropes)
- transactions
- changes
- invert changes (generates a revert)
- annotations (time changed etc)
- state effects
- additional editor state as facets
- snapshots as an async view into current state
- selections { anchor (nonmoving), head (moving) from/to } -> SelectionSet with a primary
- cursor is just a single range selection
- markers
track a position inside text that synchronizes with edits
- { doc, selection, update(), splice, changes(), facets, tabSize, identUnit, lineSeparator, changeFilter/transactionFilter to modify stuff before }
- view (actual UI)
- viewport(Lines) -> what's actually visible
- extend the view via Decorations (inline styling) or Components (UI)
- mark / wieget / line / replace decoration
- commands (transform state)
- movement
- selection extension
- deletion
- indentation
- keymap (maps keys to commands)
- history (undo tree via immutable ropes)
- undoes transactions via reverts
- (collab mode)
- gutter (line numbers, diagnostic marker, etc) -> ties into UI components
- rangeset/span -> mappable over changes (can be a marker primitive?)
- syntax (treesitter)
- fold
- selections (select mode/multiselect)
- matchbrackets
- closebrackets
- special-chars (shows dots etc for specials)
- panel (for UI: file pickers, search dialogs, etc)
- tooltip (for UI)
- search (regex)
- lint (async linters)
- lsp
- highlight
- stream-syntax
- autocomplete
- comment (gc, etc for auto commenting)
- snippets
- terminal mode?
## Core
- plugins can contain more commands/ui abstractions to use elsewhere
- languageData as presets for each language (syntax, indent, comment, etc)
The core contains basic building blocks used to construct the editor. It is
heavily based on [CodeMirror 6](https://codemirror.net/6/docs/). The primitives
are functional: most operations won't modify data in place but instead return
a new copy.
Vim stuff:
- motions/operators/text objects
- full visual mode
- macros
- jump lists
- marks
- yank/paste
- conceal for markdown markers, etc
The main data structure used for representing buffers is a `Rope`. We re-export
the excellent [ropey](https://github.com/cessen/ropey) library. Ropes are cheap
to clone, and allow us to easily make snapshots of a text state.
Multiple selections are a core editing primitive. Document selections are
represented by a `Selection`. Each `Range` in the selection consists of a moving
`head` and an immovable `anchor`. A single cursor in the editor is simply
a selection with a single range, with the head and the anchor in the same
position.
Ropes are modified by constructing an OT-like `Transaction`. It's represents
a single coherent change to the document and can be applied to the rope.
A transaction can be inverted to produce an undo. Selections and marks can be
mapped over a transaction to translate to a position in the new text state after
applying the transaction.
> NOTE: `Transaction::change`/`Transaction::change_by_selection` is the main
> interface used to generate text edits.
`Syntax` is the interface used to interact with tree-sitter ASTs for syntax
highling and other features.
## View
The `view` layer was supposed to be a frontend-agnostic imperative library that
would build on top of `core` to provide the common editor logic. Currently it's
tied to the terminal UI.
A `Document` ties together the `Rope`, `Selection`(s), `Syntax`, document
`History`, language server (etc.) into a comprehensive representation of an open
file.
A `View` represents an open split in the UI. It holds the currently open
document ID and other related state.
> NOTE: Multiple views are able to display the same document, so the document
> contains selections for each view. To retrieve, `document.selection()` takes
> a `ViewId`.
The `Editor` holds the global state: all the open documents, a tree
representation of all the view splits, and a registry of language servers. To
open or close files, interact with the editor.
## LSP
A language server protocol client.
## Term
The terminal frontend.
The `main` function sets up a new `Application` that runs the event loop.
`commands.rs` is probably the most interesting file. It contains all commands
(actions tied to keybindings).
`keymap.rs` links commands to key combinations.
## TUI / Term
TODO: document Component and rendering related stuff

26
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1619345332,
"narHash": "sha256-qHnQkEp1uklKTpx3MvKtY6xzgcqXDsz5nLilbbuL+3A=",
"lastModified": 1620759905,
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "2ebf2558e5bf978c7fb8ea927dfaed8fefab2e28",
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8",
"type": "github"
},
"original": {
@@ -50,10 +50,10 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1619775165,
"narHash": "sha256-2qaBErjxuWpTIq6Yee5GJmhr84hmzBotLQ0ayg1VXg8=",
"path": "/nix/store/gs997rgx3pvdgcb54wd3fi9wbnznd9g4-source",
"rev": "849b29b4f76d66ec7aeeeed699b7e27ef3db7c02",
"lastModified": 1622059058,
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=",
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
"type": "path"
},
"original": {
@@ -63,11 +63,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1620252427,
"narHash": "sha256-U1Q5QceuT4chJTJ1UOt7bZOn9Y2o5/7w27RISjqXoQw=",
"lastModified": 1622194753,
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d3ba49889a76539ea0f7d7285b203e7f81326ded",
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3",
"type": "github"
},
"original": {
@@ -106,11 +106,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1620355527,
"narHash": "sha256-mUTnUODiAtxH83gbv7uuvCbqZ/BNkYYk/wa3MkwrskE=",
"lastModified": 1622257069,
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d8efe70dc561c4bea0b7bf440d36ce98c497e054",
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5",
"type": "github"
},
"original": {

View File

@@ -11,7 +11,6 @@ license = "MPL-2.0"
helix-syntax = { path = "../helix-syntax" }
ropey = "1.2"
anyhow = "1"
smallvec = "1.4"
tendril = "0.4.2"
unicode-segmentation = "1.6"

View File

@@ -65,9 +65,7 @@ impl History {
self.cursor == 0
}
// TODO: I'd like to pass Transaction by reference but it fights with the borrowck
pub fn undo(&mut self) -> Option<Transaction> {
pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() {
// We're at the root of undo, nothing to do.
return None;
@@ -77,17 +75,17 @@ impl History {
self.cursor = current_revision.parent;
Some(current_revision.revert.clone())
Some(&current_revision.revert)
}
pub fn redo(&mut self) -> Option<Transaction> {
pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.cursor];
// for now, simply pick the latest child (linear undo / redo)
if let Some((index, transaction)) = current_revision.children.last() {
self.cursor = *index;
return Some(transaction.clone());
return Some(&transaction);
}
None
}

View File

@@ -13,7 +13,7 @@ mod position;
pub mod register;
pub mod search;
pub mod selection;
pub mod state;
mod state;
pub mod syntax;
mod transaction;

View File

@@ -25,8 +25,7 @@ pub fn move_horizontally(
}
Direction::Forward => {
// Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n
let end = text.line_to_char(line + 1).saturating_sub(2);
let end = text.line_to_char(line + 1).saturating_sub(1);
nth_next_grapheme_boundary(text, pos, count).min(end)
}
};
@@ -46,7 +45,7 @@ pub fn move_vertically(
let new_line = match dir {
Direction::Backward => row.saturating_sub(count),
Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 1),
Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 2),
};
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
@@ -77,8 +76,9 @@ pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) ->
begin += 1;
}
// return if not skip while?
skip_over_next(slice, &mut begin, |ch| ch == '\n');
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
return None;
};
ch = slice.char(begin);
end = begin + 1;
@@ -135,7 +135,7 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
let mut end = begin;
for _ in 0..count {
if begin + 1 == slice.len_chars() {
if begin + 2 >= slice.len_chars() {
return None;
}
@@ -146,8 +146,9 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
begin += 1;
}
// return if not skip while?
skip_over_next(slice, &mut begin, |ch| ch == '\n');
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
return None;
};
end = begin;
@@ -190,28 +191,30 @@ fn categorize(ch: char) -> Category {
Category::Eol
} else if ch.is_ascii_whitespace() {
Category::Whitespace
} else if is_word(ch) {
Category::Word
} else if ch.is_ascii_punctuation() {
Category::Punctuation
} else if ch.is_ascii_alphanumeric() {
Category::Word
} else {
unreachable!()
}
}
#[inline]
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
/// Returns true if there are more characters left after the new position.
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
where
F: Fn(char) -> bool,
{
let mut chars = slice.chars_at(*pos);
for ch in chars {
while let Some(ch) = chars.next() {
if !fun(ch) {
break;
}
*pos += 1;
}
chars.next().is_some()
}
#[inline]

View File

@@ -7,6 +7,10 @@ pub fn find_nth_next(
n: usize,
inclusive: bool,
) -> Option<usize> {
if pos >= text.len_chars() {
return None;
}
// start searching right after pos
let mut chars = text.chars_at(pos + 1);

View File

@@ -745,7 +745,7 @@ struct LocalScope<'a> {
local_defs: Vec<LocalDef<'a>>,
}
struct HighlightIter<'a, F>
struct HighlightIter<'a, 'tree: 'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@@ -753,16 +753,16 @@ where
byte_offset: usize,
injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a>>,
layers: Vec<HighlightIterLayer<'a, 'tree>>,
iter_count: usize,
next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>,
}
struct HighlightIterLayer<'a> {
struct HighlightIterLayer<'a, 'tree: 'a> {
_tree: Option<Tree>,
cursor: QueryCursor,
captures: iter::Peekable<QueryCaptures<'a, Cow<'a, [u8]>>>,
captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>,
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
@@ -929,7 +929,7 @@ impl HighlightConfiguration {
}
}
impl<'a> HighlightIterLayer<'a> {
impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
/// Create a new 'layer' of highlighting for this document.
///
/// In the even that the new layer contains "combined injections" (injections where multiple
@@ -1193,7 +1193,7 @@ impl<'a> HighlightIterLayer<'a> {
}
}
impl<'a, F> HighlightIter<'a, F>
impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@@ -1244,7 +1244,7 @@ where
}
}
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) {
if let Some(sort_key) = layer.sort_key() {
let mut i = 1;
while i < self.layers.len() {
@@ -1263,7 +1263,7 @@ where
}
}
impl<'a, F> Iterator for HighlightIter<'a, F>
impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{

View File

@@ -415,7 +415,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,

View File

@@ -107,7 +107,10 @@ fn build_dir(dir: &str, language: &str) {
}
fn main() {
let ignore = vec!["tree-sitter-typescript".to_string()];
let ignore = vec![
"tree-sitter-typescript".to_string(),
".DS_Store".to_string(),
];
let dirs = collect_tree_sitter_dirs(&ignore);
let mut n_jobs = 0;

View File

@@ -72,6 +72,7 @@ mk_langs!(
(CSharp, tree_sitter_c_sharp),
(Cpp, tree_sitter_cpp),
(Css, tree_sitter_css),
(Elixir, tree_sitter_elixir),
(Go, tree_sitter_go),
// (Haskell, tree_sitter_haskell),
(Html, tree_sitter_html),

View File

@@ -24,7 +24,6 @@ tokio = { version = "1", features = ["full"] }
num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.19", features = ["event-stream"] }
clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }

View File

@@ -1,8 +1,6 @@
use clap::ArgMatches as Args;
use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::{compositor::Compositor, ui};
use crate::{compositor::Compositor, ui, Args};
use log::{error, info};
@@ -47,8 +45,8 @@ impl Application {
let size = compositor.size();
let mut editor = Editor::new(size);
if let Ok(files) = args.values_of_t::<PathBuf>("files") {
for file in files {
if !args.files.is_empty() {
for file in args.files {
editor.open(file, Action::VerticalSplit)?;
}
} else {

View File

@@ -473,10 +473,10 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
let last_line = view.last_line(doc);
// clamp into viewport
let line = cursor.row.clamp(
view.first_line + scrolloff,
last_line.saturating_sub(scrolloff),
);
let line = cursor
.row
.min(view.first_line + scrolloff)
.max(last_line.saturating_sub(scrolloff));
let text = doc.text().slice(..);
let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
@@ -674,6 +674,7 @@ pub fn search(cx: &mut Context) {
cx.push_layer(Box::new(prompt));
}
// can't search next for ""compose"" for some reason
pub fn _search_next(cx: &mut Context, extend: bool) {
if let Some(query) = register::get('\\') {
@@ -884,17 +885,30 @@ mod cmd {
}
fn open(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let path = args[0];
editor.open(path.into(), Action::Replace);
match args.get(0) {
Some(path) => {
// TODO: handle error
editor.open(path.into(), Action::Replace);
}
None => {
editor.set_error("wrong argument count".to_string());
}
};
}
fn write(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let id = editor.view().doc;
let doc = &mut editor.documents[id];
let (view, doc) = editor.current();
if let Some(path) = args.get(0) {
if let Err(err) = doc.set_path(Path::new(path)) {
editor.set_error(format!("invalid filepath: {}", err));
return;
};
}
if doc.path().is_none() {
editor.set_error("cannot write a buffer without a filename".to_string());
return;
}
doc.format(view.id); // TODO: merge into save
tokio::spawn(doc.save());
}
@@ -905,25 +919,7 @@ mod cmd {
fn format(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let (view, doc) = editor.current();
if let Some(language_server) = doc.language_server() {
// TODO: await, no blocking
let transaction = helix_lsp::block_on(
language_server
.text_document_formatting(doc.identifier(), lsp::FormattingOptions::default()),
)
.map(|edits| {
helix_lsp::util::generate_transaction_from_edits(
doc.text(),
edits,
language_server.offset_encoding(),
)
});
if let Ok(transaction) = transaction {
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
}
}
doc.format(view.id)
}
pub const COMMAND_LIST: &[Command] = &[
@@ -951,7 +947,7 @@ mod cmd {
Command {
name: "write",
alias: Some("w"),
doc: "Write changes to disk.",
doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
fun: write,
completer: Some(completers::filename),
},
@@ -1035,6 +1031,9 @@ pub fn command_mode(cx: &mut Context) {
}
let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
if parts.is_empty() {
return;
}
if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
(cmd.fun)(editor, &parts[1..], event);
@@ -1670,6 +1669,9 @@ pub mod insert {
let head = pos + offs + text.len();
// TODO: range replace or extend
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
ranges.push(Range::new(
if range.is_empty() {
head
@@ -2273,7 +2275,8 @@ pub fn space_mode(cx: &mut Context) {
'v' => vsplit(cx),
'w' => {
// save current buffer
let doc = cx.doc();
let (view, doc) = cx.current();
doc.format(view.id); // TODO: merge into save
tokio::spawn(doc.save());
}
'c' => {

View File

@@ -122,9 +122,17 @@ impl Compositor {
}
pub fn render(&mut self, cx: &mut Context) {
let area = self.size();
let area = self
.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut();
let area = *surface.area();
for layer in &self.layers {
layer.render(area, surface, cx)
}

View File

@@ -240,10 +240,12 @@ pub fn default() -> Keymaps {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE
} => commands::page_up,
ctrl!('b') => commands::page_up,
KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE
} => commands::page_down,
ctrl!('f') => commands::page_down,
ctrl!('u') => commands::half_page_up,
ctrl!('d') => commands::half_page_down,

View File

@@ -8,12 +8,13 @@ mod ui;
use application::Application;
use clap::{App, Arg};
use helix_core::config_dir;
use std::path::PathBuf;
use anyhow::Error;
use anyhow::{Context, Error, Result};
fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
fn setup_logging(verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new();
// Let's say we depend on something which whose "info" level messages are too
@@ -28,8 +29,6 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
_3_or_more => base_config.level(log::LevelFilter::Trace),
};
let home = dirs_next::home_dir().expect("can't find the home directory");
// Separate file config so we can include year, month and day in file logs
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
@@ -41,37 +40,114 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
message
))
})
.chain(fern::log_file(home.join("helix.log"))?);
.chain(fern::log_file(config_dir().join("helix.log"))?);
base_config.chain(file_config).apply()?;
Ok(())
}
fn main() {
let args = clap::app_from_crate!()
.arg(
Arg::new("files")
.about("Sets the input file to use")
.required(false)
.multiple(true)
.index(1),
)
.arg(
Arg::new("verbose")
.about("Increases logging verbosity each use for up to 3 times")
.short('v')
.takes_value(false)
.multiple_occurrences(true),
)
.get_matches();
pub struct Args {
display_help: bool,
display_version: bool,
verbosity: u64,
files: Vec<PathBuf>,
}
let verbosity: u64 = args.occurrences_of("verbose");
fn parse_args(mut args: Args) -> Result<Args> {
let argv: Vec<String> = std::env::args().collect();
let mut iter = argv.iter();
setup_logging(verbosity).expect("failed to initialize logging.");
iter.next(); // skip the program, we don't care about that
while let Some(arg) = iter.next() {
match arg.as_str() {
"--" => break, // stop parsing at this point treat the remaining as files
"--version" => args.display_version = true,
"--help" => args.display_help = true,
arg if arg.starts_with("--") => {
return Err(Error::msg(format!(
"unexpected double dash argument: {}",
arg
)))
}
arg if arg.starts_with('-') => {
let arg = arg.get(1..).unwrap().chars();
for chr in arg {
match chr {
'v' => args.verbosity += 1,
'V' => args.display_version = true,
'h' => args.display_help = true,
_ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
}
}
}
arg => args.files.push(PathBuf::from(arg)),
}
}
// push the remaining args, if any to the files
for filename in iter {
args.files.push(PathBuf::from(filename));
}
Ok(args)
}
#[tokio::main]
async fn main() -> Result<()> {
let help = format!(
"\
{} {}
{}
{}
USAGE:
hx [FLAGS] [files]...
ARGS:
<files>... Sets the input file to use
FLAGS:
-h, --help Prints help information
-v Increases logging verbosity each use for up to 3 times
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
);
let mut args: Args = Args {
display_help: false,
display_version: false,
verbosity: 0,
files: [].to_vec(),
};
args = parse_args(args).context("could not parse arguments")?;
// Help has a higher priority and should be handled separately.
if args.display_help {
print!("{}", help);
std::process::exit(0);
}
if args.display_version {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let conf_dir = config_dir();
if !conf_dir.exists() {
std::fs::create_dir(&conf_dir);
}
setup_logging(args.verbosity).context("failed to initialize logging")?;
// initialize language registry
use helix_core::config_dir;
use helix_core::syntax::{Loader, LOADER};
// load $HOME/.config/helix/languages.toml, fallback to default config
@@ -80,17 +156,12 @@ fn main() {
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
LOADER.get_or_init(|| {
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
Loader::new(config)
});
let runtime = tokio::runtime::Runtime::new().unwrap();
let config = toml::from_slice(toml).context("Could not parse languages.toml")?;
LOADER.get_or_init(|| Loader::new(config));
// TODO: use the thread local executor to spawn the application task separately from the work pool
runtime.block_on(async move {
let mut app = Application::new(args).unwrap();
let mut app = Application::new(args).context("unable to create new appliction")?;
app.run().await;
app.run().await;
});
Ok(())
}

View File

@@ -12,11 +12,60 @@ use helix_core::{Position, Transaction};
use helix_view::Editor;
use crate::commands;
use crate::ui::{Markdown, Menu, Popup, PromptEvent};
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::lsp;
use lsp::CompletionItem;
impl menu::Item for CompletionItem {
fn filter_text(&self) -> &str {
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
}
fn label(&self) -> &str {
self.label.as_str()
}
fn row(&self) -> menu::Row {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
Some(lsp::CompletionItemKind::Text) => "text",
Some(lsp::CompletionItemKind::Method) => "method",
Some(lsp::CompletionItemKind::Function) => "function",
Some(lsp::CompletionItemKind::Constructor) => "constructor",
Some(lsp::CompletionItemKind::Field) => "field",
Some(lsp::CompletionItemKind::Variable) => "variable",
Some(lsp::CompletionItemKind::Class) => "class",
Some(lsp::CompletionItemKind::Interface) => "interface",
Some(lsp::CompletionItemKind::Module) => "module",
Some(lsp::CompletionItemKind::Property) => "property",
Some(lsp::CompletionItemKind::Unit) => "unit",
Some(lsp::CompletionItemKind::Value) => "value",
Some(lsp::CompletionItemKind::Enum) => "enum",
Some(lsp::CompletionItemKind::Keyword) => "keyword",
Some(lsp::CompletionItemKind::Snippet) => "snippet",
Some(lsp::CompletionItemKind::Color) => "color",
Some(lsp::CompletionItemKind::File) => "file",
Some(lsp::CompletionItemKind::Reference) => "reference",
Some(lsp::CompletionItemKind::Folder) => "folder",
Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
Some(lsp::CompletionItemKind::Constant) => "constant",
Some(lsp::CompletionItemKind::Struct) => "struct",
Some(lsp::CompletionItemKind::Event) => "event",
Some(lsp::CompletionItemKind::Operator) => "operator",
Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
None => "",
}),
// self.detail.as_deref().unwrap_or("")
// self.label_details
// .as_ref()
// .or(self.detail())
// .as_str(),
])
}
}
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
@@ -31,92 +80,83 @@ impl Completion {
trigger_offset: usize,
) -> Self {
// let items: Vec<CompletionItem> = Vec::new();
let mut menu = Menu::new(
items,
|item| {
// format_fn
item.label.as_str().into()
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
match event {
PromptEvent::Abort => {
// revert state
// let id = editor.view().doc;
// let doc = &mut editor.documents[id];
// doc.state = snapshot.clone();
}
PromptEvent::Validate => {
let (view, doc) = editor.current();
// TODO: use item.filter_text for filtering
},
move |editor: &mut Editor, item, event| {
match event {
PromptEvent::Abort => {
// revert state
// let id = editor.view().doc;
// let doc = &mut editor.documents[id];
// doc.state = snapshot.clone();
}
PromptEvent::Validate => {
let (view, doc) = editor.current();
// revert state to what it was before the last update
// doc.state = snapshot.clone();
// revert state to what it was before the last update
// doc.state = snapshot.clone();
// extract as fn(doc, item):
// extract as fn(doc, item):
// TODO: need to apply without composing state...
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
// the final state?
// -> on update simply update the snapshot, then on accept redo the call,
// finally updating doc.changes + notifying lsp.
//
// or we could simply use doc.undo + apply when changing between options
// TODO: need to apply without composing state...
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
// the final state?
// -> on update simply update the snapshot, then on accept redo the call,
// finally updating doc.changes + notifying lsp.
//
// or we could simply use doc.undo + apply when changing between options
// always present here
let item = item.unwrap();
// always present here
let item = item.unwrap();
use helix_lsp::{lsp, util};
// determine what to insert: text_edit | insert_text | label
let edit = if let Some(edit) = &item.text_edit {
match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
use helix_lsp::{lsp, util};
// determine what to insert: text_edit | insert_text | label
let edit = if let Some(edit) = &item.text_edit {
match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
} else {
item.insert_text.as_ref().unwrap_or(&item.label);
unimplemented!();
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
// and we insert at position.
};
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![(trigger_offset, cursor, None)].into_iter(),
);
doc.apply(&remove, view.id);
}
} else {
item.insert_text.as_ref().unwrap_or(&item.label);
unimplemented!();
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
// and we insert at position.
};
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
vec![(trigger_offset, cursor, None)].into_iter(),
);
doc.apply(&transaction, view.id);
doc.apply(&remove, view.id);
}
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits {
// gopls uses this to add extra imports
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
}
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits {
// gopls uses this to add extra imports
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
}
}
_ => (),
};
},
);
}
_ => (),
};
});
let popup = Popup::new(menu);
Self {
popup,
@@ -167,6 +207,13 @@ impl Completion {
impl Component for Completion {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
// let the Editor handle Esc instead
if let Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) = event
{
return EventResult::Ignored;
}
self.popup.handle_event(event, cx)
}
@@ -183,32 +230,61 @@ impl Component for Completion {
// option.detail
// ---
// option.documentation
match &option.documentation {
Some(lsp::Documentation::String(s))
let (view, doc) = cx.editor.current();
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
let doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: s,
value: contents,
})) => {
// TODO: convert to wrapped text
let doc = s;
Markdown::new(format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
))
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
let doc = Markdown::new(contents.clone());
let half = area.height / 2;
let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
let area = Rect::new(0, area.height - height - 2, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
doc.render(area, surface, cx);
// TODO: set language based on doc scope
Markdown::new(format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
))
}
None => (),
}
None if option.detail.is_some() => {
// TODO: copied from above
// TODO: set language based on doc scope
Markdown::new(format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
))
}
None => return,
};
let half = area.height / 2;
let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
let area = Rect::new(0, area.height - height - 2, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
doc.render(area, surface, cx);
}
}
}

View File

@@ -148,6 +148,13 @@ impl EditorView {
// TODO: scope matching: biggest union match? [string] & [html, string], [string, html] & [ string, html]
// can do this by sorting our theme matches based on array len (longest first) then stopping at the
// first rule that matches (rule.all(|scope| scopes.contains(scope)))
// log::info!(
// "scopes: {:?}",
// spans
// .iter()
// .map(|span| theme.scopes()[span.0].as_str())
// .collect::<Vec<_>>()
// );
let style = match spans.first() {
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
None => theme.get("ui.text"),
@@ -233,7 +240,7 @@ impl EditorView {
for selection in doc
.selection(view.id)
.iter()
.filter(|range| range.overlaps(&screen))
.filter(|range| screen.overlaps(&range))
{
// TODO: render also if only one of the ranges is in viewport
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
@@ -254,7 +261,7 @@ impl EditorView {
Rect::new(
viewport.x + start.col as u16,
viewport.y + start.row as u16,
(end.col - start.col) as u16 + 1,
((end.col - start.col) as u16 + 1).min(viewport.width),
1,
),
selection_style,
@@ -577,7 +584,6 @@ impl Component for EditorView {
if completion.is_empty() {
self.completion = None;
}
// TODO: if exiting InsertMode, remove completion
}
}
}
@@ -598,16 +604,24 @@ impl Component for EditorView {
let (view, doc) = cx.editor.current();
view.ensure_cursor_in_view(doc);
if mode == Mode::Normal && doc.mode() == Mode::Insert {
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
// mode transitions
match (mode, doc.mode()) {
(Mode::Normal, Mode::Insert) => {
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
self.last_insert.0 = self.keymap[&mode][&key];
self.last_insert.1.clear();
};
self.last_insert.0 = self.keymap[&mode][&key];
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion
self.completion = None;
}
_ => (),
}
EventResult::Consumed(callback)
}
@@ -619,6 +633,10 @@ impl Component for EditorView {
// clear with background color
surface.set_style(area, cx.editor.theme.get("ui.background"));
// if the terminal size suddenly changed, we need to trigger a resize
cx.editor
.resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline
for (view, is_focused) in cx.editor.tree.views() {
let doc = cx.editor.document(view.doc).unwrap();
self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused);

View File

@@ -107,11 +107,14 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
None => text_style,
};
// TODO: replace tabs with indentation
let mut slice = &text[start..end];
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let span = Span::styled(text.to_owned(), style);
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
// truncate slice to after newline
@@ -124,7 +127,8 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
// if there's anything left, emit it too
if !slice.is_empty() {
let span = Span::styled(slice.to_owned(), style);
let span =
Span::styled(slice.replace('\t', " "), style);
spans.push(span);
}
}
@@ -153,6 +157,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
}
Event::Code(text) | Event::Html(text) => {
log::warn!("code {:?}", text);
let mut span = to_span(text);
span.style = code_style;
spans.push(span);
@@ -167,7 +172,9 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
lines.push(Spans::default());
}
// TaskListMarker(bool) true if checked
_ => (),
_ => {
log::warn!("unhandled markdown event {:?}", event);
}
}
// build up a vec of Paragraph tui widgets
}

View File

@@ -4,8 +4,11 @@ use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::Table,
};
pub use tui::widgets::{Cell, Row};
use std::borrow::Cow;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@@ -14,7 +17,15 @@ use fuzzy_matcher::FuzzyMatcher;
use helix_core::Position;
use helix_view::Editor;
pub struct Menu<T> {
pub trait Item {
// TODO: sort_text
fn filter_text(&self) -> &str;
fn label(&self) -> &str;
fn row(&self) -> Row;
}
pub struct Menu<T: Item> {
options: Vec<T>,
cursor: Option<usize>,
@@ -23,19 +34,17 @@ pub struct Menu<T> {
/// (index, score)
matches: Vec<(usize, i64)>,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
scroll: usize,
size: (u16, u16),
}
impl<T> Menu<T> {
impl<T: Item> Menu<T> {
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
// rendering)
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
@@ -43,7 +52,6 @@ impl<T> Menu<T> {
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
cursor: None,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
@@ -61,7 +69,6 @@ impl<T> Menu<T> {
ref mut options,
ref mut matcher,
ref mut matches,
ref format_fn,
..
} = *self;
@@ -72,8 +79,7 @@ impl<T> Menu<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
// TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option);
let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
.fuzzy_match(&text, pattern)
@@ -134,7 +140,7 @@ impl<T> Menu<T> {
use super::PromptEvent as MenuEvent;
impl<T: 'static> Component for Menu<T> {
impl<T: Item + 'static> Component for Menu<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event {
Event::Key(event) => event,
@@ -224,7 +230,7 @@ impl<T: 'static> Component for Menu<T> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let width = std::cmp::min(30, viewport.0);
const MAX: usize = 5;
const MAX: usize = 10;
let height = std::cmp::min(self.options.len(), MAX);
let height = std::cmp::min(height, viewport.1 as usize);
@@ -236,9 +242,11 @@ impl<T: 'static> Component for Menu<T> {
Some(self.size)
}
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.text");
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
let selected = cx.editor.theme.get("ui.menu.selected");
let scroll = self.scroll;
@@ -264,26 +272,40 @@ impl<T: 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
for (i, option) in options[scroll..(scroll + win_height).min(len)]
.iter()
.enumerate()
{
let line = Some(i + scroll);
// TODO: set bg for the whole row if selected
surface.set_stringn(
area.x,
area.y + i as u16,
(self.format_fn)(option),
area.width as usize - 1,
if line == self.cursor { selected } else { style },
);
use tui::layout::Constraint;
let rows = options.iter().map(|option| option.row());
let table = Table::new(rows)
.style(style)
.highlight_style(selected)
.column_spacing(1)
.widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]);
use tui::widgets::TableState;
table.render_table(
area,
surface,
&mut TableState {
offset: scroll,
selected: self.cursor,
},
);
// // TODO: set bg for the whole row if selected
// if line == self.cursor {
// surface.set_style(
// Rect::new(area.x, area.y + i as u16, area.width - 1, 1),
// selected,
// )
// }
for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
if is_marked {
let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
cell.set_symbol("");
cell.set_style(selected);
// cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style });
}
}

View File

@@ -118,6 +118,18 @@ impl<T> Picker<T> {
// - on input change:
// - score all the names in relation to input
fn inner_rect(area: Rect) -> Rect {
let padding_vertical = area.height * 20 / 100;
let padding_horizontal = area.width * 20 / 100;
Rect::new(
area.x + padding_horizontal,
area.y + padding_vertical,
area.width - padding_horizontal * 2,
area.height - padding_vertical * 2,
)
}
impl<T: 'static> Component for Picker<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
@@ -191,15 +203,7 @@ impl<T: 'static> Component for Picker<T> {
}
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let padding_vertical = area.height * 20 / 100;
let padding_horizontal = area.width * 20 / 100;
let area = Rect::new(
area.x + padding_horizontal,
area.y + padding_vertical,
area.width - padding_horizontal * 2,
area.height - padding_vertical * 2,
);
let area = inner_rect(area);
// -- Render the frame:
@@ -260,6 +264,15 @@ impl<T: 'static> Component for Picker<T> {
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
// TODO: this is mostly duplicate code
let area = inner_rect(area);
let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box
let inner = block.inner(area);
// prompt area
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
self.prompt.cursor_position(area, editor)
}
}

View File

@@ -111,6 +111,7 @@ impl Prompt {
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
let text_color = theme.get("ui.text.focus");
let selected_color = theme.get("ui.menu.selected");
// completion
let max_col = area.width / BASE_WIDTH;
@@ -133,7 +134,8 @@ impl Prompt {
for (i, (_range, completion)) in self.completion.iter().enumerate() {
let color = if Some(i) == self.selection {
Style::default().bg(Color::Rgb(104, 60, 232))
// Style::default().bg(Color::Rgb(104, 60, 232))
selected_color // TODO: just invert bg
} else {
text_color
};
@@ -158,14 +160,15 @@ impl Prompt {
if let Some(doc) = (self.doc_fn)(&self.line) {
let text = ui::Text::new(doc.to_string());
let area = Rect::new(
let viewport = area;
let area = viewport.intersection(Rect::new(
completion_area.x,
completion_area.y - 3,
completion_area.width,
BASE_WIDTH * 3,
3,
);
));
let background = theme.get("ui.window");
let background = theme.get("ui.help");
surface.clear_with(area, background);
use tui::layout::Margin;
@@ -271,8 +274,9 @@ impl Component for Prompt {
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
let line = area.height as usize - 1;
Some(Position::new(
area.height as usize,
area.y as usize + line,
area.x as usize + self.prompt.len() + self.cursor,
))
}

View File

@@ -137,14 +137,12 @@ where
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
}
pub fn autoresize(&mut self) -> io::Result<Rect> {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
};
Ok(())
Ok(size)
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state

View File

@@ -13,12 +13,12 @@ mod block;
// mod list;
mod paragraph;
mod reflow;
// mod table;
mod table;
pub use self::block::{Block, BorderType};
// pub use self::list::{List, ListItem, ListState};
pub use self::paragraph::{Paragraph, Wrap};
// pub use self::table::{Cell, Row, Table, TableState};
pub use self::table::{Cell, Row, Table, TableState};
use crate::{buffer::Buffer, layout::Rect};
use bitflags::bitflags;

View File

@@ -3,7 +3,7 @@ use crate::{
layout::{Constraint, Rect},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
widgets::{Block, Widget},
};
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
@@ -368,8 +368,8 @@ impl<'a> Table<'a> {
#[derive(Debug, Clone)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
pub offset: usize,
pub selected: Option<usize>,
}
impl Default for TableState {
@@ -394,10 +394,11 @@ impl TableState {
}
}
impl<'a> StatefulWidget for Table<'a> {
type State = TableState;
// impl<'a> StatefulWidget for Table<'a> {
impl<'a> Table<'a> {
// type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
if area.area() == 0 {
return;
}
@@ -522,7 +523,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
Table::render_table(self, area, buf, &mut state);
}
}

View File

@@ -1,6 +1,7 @@
use anyhow::{Context, Error};
use std::cell::Cell;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use helix_core::{
@@ -40,7 +41,10 @@ pub struct Document {
/// State at last commit. Used for calculating reverts.
old_state: Option<State>,
/// Undo tree.
history: History,
// It can be used as a cell where we will take it out to get some parts of the history and put
// it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome.
history: Cell<History>,
last_saved_revision: usize,
version: i32, // should be usize?
@@ -64,6 +68,42 @@ where
}
}
/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
use helix_lsp::lsp;
use url::Url;
@@ -85,7 +125,7 @@ impl Document {
old_state,
diagnostics: Vec::new(),
version: 0,
history: History::default(),
history: Cell::new(History::default()),
last_saved_revision: 0,
language_server: None,
}
@@ -116,6 +156,29 @@ impl Document {
Ok(doc)
}
// TODO: remove view_id dependency here
pub fn format(&mut self, view_id: ViewId) {
if let Some(language_server) = self.language_server() {
// TODO: await, no blocking
let transaction = helix_lsp::block_on(
language_server
.text_document_formatting(self.identifier(), lsp::FormattingOptions::default()),
)
.map(|edits| {
helix_lsp::util::generate_transaction_from_edits(
self.text(),
edits,
language_server.offset_encoding(),
)
});
if let Ok(transaction) = transaction {
self.apply(&transaction, view_id);
self.append_changes_to_history(view_id);
}
}
}
// TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
// or is that handled by the OS/async layer
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -131,7 +194,9 @@ impl Document {
let language_server = self.language_server.clone();
// reset the modified flag
self.last_saved_revision = self.history.current_revision();
let history = self.history.take();
self.last_saved_revision = history.current_revision();
self.history.set(history);
async move {
use tokio::{fs::File, io::AsyncWriteExt};
@@ -153,6 +218,20 @@ impl Document {
}
}
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
// canonicalize path to absolute value
let current_dir = std::env::current_dir()?;
let path = normalize_path(&current_dir.join(path));
if let Some(parent) = path.parent() {
// TODO: return error as necessary
if parent.exists() {
self.path = Some(path);
}
}
Ok(())
}
pub fn set_language(
&mut self,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
@@ -262,7 +341,8 @@ impl Document {
}
pub fn undo(&mut self, view_id: ViewId) -> bool {
if let Some(transaction) = self.history.undo() {
let mut history = self.history.take();
if let Some(transaction) = history.undo() {
let success = self._apply(&transaction, view_id);
// reset changeset to fix len
@@ -270,11 +350,13 @@ impl Document {
return success;
}
self.history.set(history);
false
}
pub fn redo(&mut self, view_id: ViewId) -> bool {
if let Some(transaction) = self.history.redo() {
let mut history = self.history.take();
if let Some(transaction) = history.redo() {
let success = self._apply(&transaction, view_id);
// reset changeset to fix len
@@ -282,6 +364,7 @@ impl Document {
return success;
}
self.history.set(history);
false
}
@@ -300,7 +383,9 @@ impl Document {
// HAXX: we need to reconstruct the state as it was before the changes..
let old_state = self.old_state.take().expect("no old_state available");
self.history.commit_revision(&transaction, &old_state);
let mut history = self.history.take();
history.commit_revision(&transaction, &old_state);
self.history.set(history);
}
#[inline]
@@ -310,9 +395,11 @@ impl Document {
#[inline]
pub fn is_modified(&self) -> bool {
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
self.path.is_some()
&& (self.history.current_revision() != self.last_saved_revision
|| !self.changes.is_empty())
&& (current_revision != self.last_saved_revision || !self.changes.is_empty())
}
#[inline]

View File

@@ -194,8 +194,9 @@ impl Editor {
}
pub fn resize(&mut self, area: Rect) {
self.tree.resize(area);
self._refresh();
if self.tree.resize(area) {
self._refresh();
};
}
pub fn focus_next(&mut self) {

View File

@@ -293,9 +293,13 @@ impl Tree {
}
}
pub fn resize(&mut self, area: Rect) {
self.area = area;
self.recalculate();
pub fn resize(&mut self, area: Rect) -> bool {
if self.area != area {
self.area = area;
self.recalculate();
return true;
}
false
}
pub fn recalculate(&mut self) {

View File

@@ -143,8 +143,9 @@ impl View {
}
}
let row = line - self.first_line as usize;
let col = col - self.first_col as usize;
// It is possible for underflow to occur if the buffer length is larger than the terminal width.
let row = line.saturating_sub(self.first_line);
let col = col.saturating_sub(self.first_col);
Some(Position::new(row, col))
}

View File

@@ -17,6 +17,15 @@ roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "elixir"
scope = "source.elixir"
injection-regex = "elixir"
file-types = ["ex", "exs"]
roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "json"
scope = "source.json"
@@ -65,6 +74,17 @@ roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "typescript"
scope = "source.ts"
injection-regex = "^(ts|typescript)$"
file-types = ["ts"]
roots = []
# TODO: highlights-jsx, highlights-params
language-server = { command = "typescript-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " }
[[language]]
name = "css"
scope = "source.css"

View File

@@ -0,0 +1,146 @@
["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
[(true) (false) (nil)] @constant.builtin
(keyword
[(keyword_literal)
":"] @tag)
(keyword
(keyword_string
[(string_start)
(string_content)
(string_end)] @tag))
[(atom_literal)
(atom_start)
(atom_content)
(atom_end)] @tag
(comment) @comment
(escape_sequence) @escape
(call function: (function_identifier) @keyword
(#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$"))
(call function: (function_identifier) @keyword
[(call
function: (function_identifier) @function
(arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
(binary_op
left:
(call
function: (function_identifier) @function
(arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
operator: "when")
(binary_op
left: (identifier) @variable.parameter
operator: _ @function
right: (identifier) @variable.parameter)]
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$")
(#match? @variable.parameter "^[^_]"))
(call (function_identifier) @keyword
[(call
function: (function_identifier) @function)
(identifier) @function
(binary_op
left:
[(call
function: (function_identifier) @function)
(identifier) @function]
operator: "when")]
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
(anonymous_function
(stab_expression
left: (bare_arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
(#match? @variable.parameter "^[^_]"))
(unary_op
operator: "@"
(call (identifier) @attribute
(heredoc
[(heredoc_start)
(heredoc_content)
(heredoc_end)] @doc))
(#match? @attribute "^(doc|moduledoc)$"))
(module) @type
(unary_op
operator: "@" @attribute
[(call
function: (function_identifier) @attribute)
(identifier) @attribute])
(unary_op
operator: _ @operator)
(binary_op
operator: _ @operator)
(heredoc
[(heredoc_start)
(heredoc_content)
(heredoc_end)] @string)
(string
[(string_start)
(string_content)
(string_end)] @string)
(sigil_start) @string.special
(sigil_content) @string
(sigil_end) @string.special
(interpolation
"#{" @punctuation.special
"}" @punctuation.special)
[
","
"->"
"."
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
"<<"
">>"
] @punctuation.bracket
[(identifier) @function.special
(#match? @function.special "^__.+__$")]
[(remote_identifier) @function.special
(#match? @function.special "^__.+__$")]
[(identifier) @comment
(#match? @comment "^_")]
(ERROR) @warning

View File

@@ -3,15 +3,19 @@ indent = [
"const_declaration",
"var_declaration",
"type_declaration",
"function_declaration",
"method_declaration",
"type_spec",
# simply block should be enough
# "function_declaration",
# "method_declaration",
"composite_literal",
"func_literal",
"literal_value",
"expression_case",
"default_case",
"type_case",
"communication_case",
"argument_list",
"block"
"block",
]
outdent = [

View File

@@ -0,0 +1,28 @@
indent = [
"array",
"object",
"arguments",
"formal_parameters",
"statement_block",
"object_pattern",
"class_body",
"named_imports",
"binary_expression",
"return_statement",
"template_substitution",
# (expression_statement (call_expression))
"export_clause",
# typescript
"enum_declaration",
"interface_declaration",
"object_type",
]
outdent = [
"}",
"]",
")"
]

View File

@@ -19,7 +19,8 @@ indent = [
"enum_variant_list",
"binary_expression",
"field_expression",
"where_clause"
"where_clause",
"macro_invocation"
]
outdent = [

View File

@@ -0,0 +1,36 @@
; inherits: javascript
; Types
(type_identifier) @type
(predefined_type) @type.builtin
((identifier) @type
(#match? @type "^[A-Z]"))
(type_arguments
"<" @punctuation.bracket
">" @punctuation.bracket)
; Variables
(required_parameter (identifier) @variable.parameter)
(optional_parameter (identifier) @variable.parameter)
; Keywords
[
"abstract"
"declare"
"enum"
"export"
"implements"
"interface"
"keyof"
"namespace"
"private"
"protected"
"public"
"type"
"readonly"
] @keyword

View File

@@ -0,0 +1 @@
../javascript/indents.toml

View File

@@ -0,0 +1,2 @@
(required_parameter (identifier) @local.definition)
(optional_parameter (identifier) @local.definition)

View File

@@ -0,0 +1,23 @@
(function_signature
name: (identifier) @name) @definition.function
(method_signature
name: (property_identifier) @name) @definition.method
(abstract_method_signature
name: (property_identifier) @name) @definition.method
(abstract_class_declaration
name: (type_identifier) @name) @definition.class
(module
name: (identifier) @name) @definition.module
(interface_declaration
name: (type_identifier) @name) @definition.interface
(type_annotation
(type_identifier) @name) @reference.type
(new_expression
constructor: (identifier) @name) @reference.class

View File

@@ -4,6 +4,8 @@ pkgs.mkShell {
nativeBuildInputs = with pkgs; [
(rust-bin.stable.latest.default.override { extensions = ["rust-src"]; })
lld_10
lldb
# pythonPackages.six
stdenv.cc.cc.lib
# pkg-config
];
@@ -12,6 +14,7 @@ pkgs.mkShell {
# https://github.com/rust-lang/rust/issues/55979
LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib64:$LD_LIBRARY_PATH";
HELIX_RUNTIME=./runtime;
shellHook = ''
export HELIX_RUNTIME=$PWD/runtime
'';
}

View File

@@ -43,10 +43,15 @@
"ui.statusline" = { bg = "#281733" } # revolver
"ui.popup" = { bg = "#281733" } # revolver
"ui.window" = { bg = "#452859" } # bossa nova
"ui.window" = { bg = "#452859" } # bossa nova
"ui.help" = { bg = "#6F44F0", fg = "#a4a0e8" }
"ui.help" = { bg = "#7958DC", fg = "#171452" }
"ui.text" = { fg = "#a4a0e8"} # lavender
"ui.text" = { fg = "#a4a0e8" } # lavender
"ui.text.focus" = { fg = "#dbbfef"} # lilac
"ui.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver
"warning" = "#ffcd1c"
"error" = "#f47868"
"info" = "#6F44F0"