mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8fd8006043 | ||
|
ce25aa951e | ||
|
a2147fc7d5 | ||
|
d8e16554bf | ||
|
2cc30cd07c | ||
|
0dde5f2cae | ||
|
b8d6e6ad28 | ||
|
5825bce0e4 | ||
|
aeabfc55a8 | ||
|
17e9386388 | ||
|
138787f76e | ||
|
1132c5122f | ||
|
4a8053e832 | ||
|
e033a4b8ac | ||
|
acbcd758bd | ||
|
76eed4caad | ||
|
3170c49be8 | ||
|
0327d66653 | ||
|
c67e31830d | ||
|
6460501a44 | ||
|
67b037050f | ||
|
87d0617f3b | ||
|
668f735232 | ||
|
a3a9502596 | ||
|
3810650a6b | ||
|
2c48d65b15 | ||
|
d5466eddf5 | ||
|
d54ae09d3b | ||
|
a28eaa81a0 | ||
|
d708efe275 | ||
|
3336023614 | ||
|
094203c74e | ||
|
b114cfa119 |
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal 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"
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -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
27
.github/workflows/gh-pages.yml
vendored
Normal 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
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -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",
|
||||
@@ -349,6 +326,7 @@ dependencies = [
|
||||
"log",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"pico-args",
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -423,16 +401,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 +445,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 +469,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 +570,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"
|
||||
@@ -645,6 +607,12 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.6"
|
||||
@@ -659,9 +627,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 +825,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 +880,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 +931,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 +1003,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"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Helix
|
||||
|
||||
|
||||
[](https://github.com/helix-editor/helix/actions)
|
||||
[](https://github.com/helix-editor/helix/actions)
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,5 +59,5 @@ a good overview of the internals.
|
||||
|
||||
# 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).
|
||||
|
||||
|
5
TODO.md
5
TODO.md
@@ -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)
|
||||
|
@@ -21,7 +21,7 @@ shell for working on Helix.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
TODO: AUR
|
||||
A binary package is available on AUR as [helix-bin](https://aur.archlinux.org/packages/helix-bin/).
|
||||
|
||||
## Build from source
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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%);
|
||||
|
@@ -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
26
flake.lock
generated
@@ -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": {
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
};
|
||||
@@ -190,10 +189,10 @@ 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!()
|
||||
}
|
||||
|
@@ -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,
|
||||
{
|
||||
|
@@ -24,7 +24,7 @@ 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"] }
|
||||
pico-args = "0.4"
|
||||
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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),
|
||||
},
|
||||
@@ -1670,6 +1666,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 +2272,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' => {
|
||||
|
@@ -8,7 +8,6 @@ mod ui;
|
||||
|
||||
use application::Application;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Error;
|
||||
@@ -48,28 +47,54 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
|
||||
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 {
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
let verbosity: u64 = args.occurrences_of("verbose");
|
||||
fn main() {
|
||||
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 pargs = pico_args::Arguments::from_env();
|
||||
|
||||
// Help has a higher priority and should be handled separately.
|
||||
if pargs.contains(["-h", "--help"]) {
|
||||
print!("{}", help);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let mut verbosity: u64 = 0;
|
||||
|
||||
if pargs.contains("-v") {
|
||||
verbosity = 1;
|
||||
}
|
||||
|
||||
setup_logging(verbosity).expect("failed to initialize logging.");
|
||||
|
||||
let args = Args {
|
||||
files: pargs.finish().into_iter().map(|arg| arg.into()).collect(),
|
||||
};
|
||||
|
||||
// initialize language registry
|
||||
use helix_core::config_dir;
|
||||
use helix_core::syntax::{Loader, LOADER};
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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"),
|
||||
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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,9 @@ impl Prompt {
|
||||
if let Some(doc) = (self.doc_fn)(&self.line) {
|
||||
let text = ui::Text::new(doc.to_string());
|
||||
|
||||
let area = Rect::new(
|
||||
completion_area.x,
|
||||
completion_area.y - 3,
|
||||
completion_area.width,
|
||||
3,
|
||||
);
|
||||
let area = Rect::new(completion_area.x, completion_area.y - 3, 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 +268,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,
|
||||
))
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context, Error};
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use helix_core::{
|
||||
@@ -64,6 +64,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;
|
||||
|
||||
@@ -116,6 +152,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>> {
|
||||
@@ -153,6 +212,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(¤t_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>>,
|
||||
|
@@ -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 = [
|
||||
|
@@ -19,7 +19,8 @@ indent = [
|
||||
"enum_variant_list",
|
||||
"binary_expression",
|
||||
"field_expression",
|
||||
"where_clause"
|
||||
"where_clause",
|
||||
"macro_invocation"
|
||||
]
|
||||
|
||||
outdent = [
|
||||
|
@@ -12,6 +12,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;
|
||||
# HELIX_RUNTIME=./runtime;
|
||||
HELIX_RUNTIME="/home/speed/src/helix/runtime";
|
||||
}
|
||||
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user