mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
59f94d13b8 | ||
|
b3eeac7bbf | ||
|
f48a60b8e2 | ||
|
4f561e93b8 | ||
|
01b1bd15a1 | ||
|
ff8a031cb2 | ||
|
d9b2f6feac | ||
|
582f1ee9d8 | ||
|
e2d780f993 | ||
|
843c2cdebd | ||
|
8a29086c1a | ||
|
16b1cfa3be | ||
|
2066e866c7 | ||
|
3494bb8ef0 | ||
|
a4ff8cdd8a | ||
|
145bc1970a | ||
|
54f3548d54 | ||
|
3280510d5b | ||
|
df80f3c966 | ||
|
40744ce835 | ||
|
aa8a8baeeb | ||
|
bcb1afeb4c | ||
|
de946d2357 | ||
|
14f511da93 | ||
|
392631b21d | ||
|
ce99ecc7a2 | ||
|
2ac496f919 | ||
|
5463a436a8 | ||
|
e09b0f4eff | ||
|
f3db12e240 | ||
|
676719b361 | ||
|
ae105812d6 | ||
|
255598a2cb | ||
|
62d181de78 | ||
|
8c2fa12ffc | ||
|
212f6bc372 | ||
|
c5c3ec07f4 | ||
|
444cd0b068 | ||
|
f6a900fee1 | ||
|
6254720f53 | ||
|
407b37c327 | ||
|
2bb71a829e | ||
|
c17dcb8633 | ||
|
5a344a3ae5 | ||
|
a1f4b8f92b | ||
|
72eaaaac99 | ||
|
8f78c0c612 | ||
|
01dd7b570a | ||
|
f3a243c6cb | ||
|
adcfcf9044 | ||
|
4f0e3aa948 | ||
|
f2e554d761 | ||
|
bd4552cd2b | ||
|
06d8d3f55f | ||
|
8afd4e1bc2 | ||
|
43b92b24d2 | ||
|
b2b2d430ae | ||
|
8af5a9a5cf | ||
|
f76f44c8af | ||
|
d55419604c | ||
|
29b9eed33c | ||
|
fdb5bfafae | ||
|
e6132f0acd | ||
|
3071339cbc | ||
|
27aee705e0 | ||
|
f0fe558f38 | ||
|
09a7db637e | ||
|
31ed4db153 | ||
|
3c5dfb0633 | ||
|
6cbc0aea92 | ||
|
c1c3750d38 | ||
|
daad8ebe12 | ||
|
68abc67ec6 | ||
|
712f25c2b9 | ||
|
abe8a83d8e | ||
|
a05fb95769 | ||
|
74e4ac8d49 | ||
|
0e6f007028 | ||
|
c3a98b6a3e | ||
|
4fe654cf9a | ||
|
661dbdca57 | ||
|
5773bd6a40 | ||
|
d664d1dec0 |
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -1,11 +1,11 @@
|
|||||||
name: Build
|
name: Build
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
schedule:
|
||||||
branches:
|
- cron: '00 01 * * *'
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test Suite
|
name: Test Suite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: stable
|
toolchain: ${{ matrix.rust }}
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
@@ -86,6 +86,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
rust: [stable]
|
||||||
|
|
||||||
lints:
|
lints:
|
||||||
name: Lints
|
name: Lints
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -37,6 +37,10 @@ jobs:
|
|||||||
rust: stable
|
rust: stable
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
cross: false
|
cross: false
|
||||||
|
# - build: aarch64-macos
|
||||||
|
# os: macos-latest
|
||||||
|
# rust: stable
|
||||||
|
# target: aarch64-apple-darwin
|
||||||
# - build: x86_64-win-gnu
|
# - build: x86_64-win-gnu
|
||||||
# os: windows-2019
|
# os: windows-2019
|
||||||
# rust: stable-x86_64-gnu
|
# rust: stable-x86_64-gnu
|
||||||
|
47
Cargo.lock
generated
47
Cargo.lock
generated
@@ -259,13 +259,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-core"
|
name = "helix-core"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"helix-syntax",
|
"helix-syntax",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"ropey",
|
"ropey",
|
||||||
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tendril",
|
"tendril",
|
||||||
@@ -277,7 +278,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-lsp"
|
name = "helix-lsp"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
@@ -299,7 +300,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-syntax"
|
name = "helix-syntax"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -309,7 +310,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-term"
|
name = "helix-term"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -335,7 +336,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-tui"
|
name = "helix-tui"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
@@ -347,13 +348,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-view"
|
name = "helix-view"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"helix-core",
|
"helix-core",
|
||||||
"helix-lsp",
|
"helix-lsp",
|
||||||
"helix-tui",
|
"helix-tui",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"serde",
|
"serde",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
@@ -692,6 +694,39 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "5.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "5.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "5.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d"
|
||||||
|
dependencies = [
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
32
README.md
32
README.md
@@ -13,6 +13,8 @@ myself agreeing with most of kakoune's design decisions.
|
|||||||
For more information, see the [website](https://helix-editor.com) or
|
For more information, see the [website](https://helix-editor.com) or
|
||||||
[documentation](https://docs.helix-editor.com/).
|
[documentation](https://docs.helix-editor.com/).
|
||||||
|
|
||||||
|
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html)
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Vim-like modal editing
|
- Vim-like modal editing
|
||||||
@@ -25,7 +27,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Note: Only Rust and Golang have indentation definitions at the moment.
|
Note: Only certain languages have indentation definitions at the moment. Check
|
||||||
|
`runtime/<lang>/` for `indents.toml`.
|
||||||
|
|
||||||
We provide packaging for various distributions, but here's a quick method to
|
We provide packaging for various distributions, but here's a quick method to
|
||||||
build from source.
|
build from source.
|
||||||
@@ -44,12 +47,29 @@ the `HELIX_RUNTIME` environment variable.
|
|||||||
|
|
||||||
> NOTE: You should set this to <path to repository>/runtime in development (if
|
> NOTE: You should set this to <path to repository>/runtime in development (if
|
||||||
> running via cargo).
|
> running via cargo).
|
||||||
|
>
|
||||||
|
> `export HELIX_RUNTIME=$PWD/runtime`
|
||||||
|
|
||||||
|
If you want to embed the `runtime/` directory into the Helix binary you can build
|
||||||
|
it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install --path helix-term --features "embed_runtime"
|
||||||
|
```
|
||||||
|
|
||||||
## Arch Linux
|
## Arch Linux
|
||||||
There are two packages available from AUR:
|
There are two packages available from AUR:
|
||||||
- `helix-bin`: contains prebuilt binary from GitHub releases
|
- `helix-bin`: contains prebuilt binary from GitHub releases
|
||||||
- `helix-git`: builds the master branch of this repository
|
- `helix-git`: builds the master branch of this repository
|
||||||
|
|
||||||
|
## MacOS
|
||||||
|
Helix can be installed on MacOS through homebrew via:
|
||||||
|
|
||||||
|
```
|
||||||
|
brew tap helix-editor/helix
|
||||||
|
brew install helix
|
||||||
|
```
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
||||||
@@ -65,14 +85,6 @@ Some suggestions to get started:
|
|||||||
We provide an [architecture.md](./docs/architecture.md) that should give you
|
We provide an [architecture.md](./docs/architecture.md) that should give you
|
||||||
a good overview of the internals.
|
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
|
# Getting help
|
||||||
|
|
||||||
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-community:matrix.org).
|
Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).
|
||||||
|
|
||||||
|
@@ -1 +1,87 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
## Theme
|
||||||
|
|
||||||
|
Use a custom theme by placing a theme.toml in your config directory (i.e ~/.config/helix/theme.toml). The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/contrib/themes).
|
||||||
|
|
||||||
|
Styles in theme.toml are specified of in the form:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
where `name` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
|
||||||
|
|
||||||
|
To specify only the foreground color:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
key = "#ffffff"
|
||||||
|
```
|
||||||
|
|
||||||
|
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
"key.key" = "#ffffff"
|
||||||
|
```
|
||||||
|
|
||||||
|
Possible modifiers:
|
||||||
|
|
||||||
|
| modifier |
|
||||||
|
| --- |
|
||||||
|
| bold |
|
||||||
|
| dim |
|
||||||
|
| italic |
|
||||||
|
| underlined |
|
||||||
|
| slow\_blink |
|
||||||
|
| rapid\_blink |
|
||||||
|
| reversed |
|
||||||
|
| hidden |
|
||||||
|
| crossed\_out |
|
||||||
|
|
||||||
|
Possible keys:
|
||||||
|
|
||||||
|
| key | notes |
|
||||||
|
| --- | --- |
|
||||||
|
| attribute | |
|
||||||
|
| keyword | |
|
||||||
|
| keyword.directive | preprocessor directives (\#if in C) |
|
||||||
|
| namespace | |
|
||||||
|
| punctuation | |
|
||||||
|
| punctuation.delimiter | |
|
||||||
|
| operator | |
|
||||||
|
| special | |
|
||||||
|
| property | |
|
||||||
|
| variable | |
|
||||||
|
| variable.parameter | |
|
||||||
|
| type | |
|
||||||
|
| type.builtin | |
|
||||||
|
| constructor | |
|
||||||
|
| function | |
|
||||||
|
| function.macro | |
|
||||||
|
| function.builtin | |
|
||||||
|
| comment | |
|
||||||
|
| variable.builtin | |
|
||||||
|
| constant | |
|
||||||
|
| constant.builtin | |
|
||||||
|
| string | |
|
||||||
|
| number | |
|
||||||
|
| escape | escaped characters |
|
||||||
|
| label | used for lifetimes |
|
||||||
|
| module | |
|
||||||
|
| ui.background | |
|
||||||
|
| ui.linenr | |
|
||||||
|
| ui.statusline | |
|
||||||
|
| ui.popup | |
|
||||||
|
| ui.window | |
|
||||||
|
| ui.help | |
|
||||||
|
| ui.text | |
|
||||||
|
| ui.text.focus | |
|
||||||
|
| ui.menu.selected | |
|
||||||
|
| warning | LSP warning |
|
||||||
|
| error | LSP error |
|
||||||
|
| info | LSP info |
|
||||||
|
| hint | LSP hint |
|
||||||
|
|
||||||
|
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
|
||||||
|
|
||||||
|
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.
|
||||||
|
@@ -35,3 +35,10 @@ This will install the `hx` binary to `$HOME/.cargo/bin`.
|
|||||||
Now copy the `runtime/` directory somewhere. Helix will by default look for the
|
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
|
runtime inside the same folder as the executable, but that can be overriden via
|
||||||
the `HELIX_RUNTIME` environment variable.
|
the `HELIX_RUNTIME` environment variable.
|
||||||
|
|
||||||
|
If you want to embed the `runtime/` directory into the Helix binary you can build
|
||||||
|
it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install --path helix-term --features "embed_runtime"
|
||||||
|
```
|
||||||
|
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
|-----|-----------|
|
|-----|-----------|
|
||||||
| h | move left |
|
| h, Left | move left |
|
||||||
| j | move down |
|
| j, Down | move down |
|
||||||
| k | move up |
|
| k, Up | move up |
|
||||||
| l | move right |
|
| l, Right | move right |
|
||||||
| w | move next word start |
|
| w | move next word start |
|
||||||
| b | move previous word start |
|
| b | move previous word start |
|
||||||
| e | move next word end |
|
| e | move next word end |
|
||||||
@@ -17,20 +17,20 @@
|
|||||||
| f | find next char |
|
| f | find next char |
|
||||||
| T | find 'till previous char |
|
| T | find 'till previous char |
|
||||||
| F | find previous char |
|
| F | find previous char |
|
||||||
| ^ | move to the start of the line |
|
| Home | move to the start of the line |
|
||||||
| $ | move to the end of the line |
|
| End | move to the end of the line |
|
||||||
| m | Jump to matching bracket |
|
| m | Jump to matching bracket |
|
||||||
| PageUp | Move page up |
|
| PageUp | Move page up |
|
||||||
| PageDown | Move page down |
|
| PageDown | Move page down |
|
||||||
| ctrl-u | Move half page up |
|
| ctrl-u | Move half page up |
|
||||||
| ctrl-d | Move half page down |
|
| ctrl-d | Move half page down |
|
||||||
| Tab | Switch to next view |
|
|
||||||
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
|
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
|
||||||
| ctrl-o | Jump backward on the jumplist |
|
| ctrl-o | Jump backward on the jumplist |
|
||||||
| v | Enter select (extend) mode |
|
| v | Enter select (extend) mode |
|
||||||
| g | Enter goto mode |
|
| g | Enter goto mode |
|
||||||
| : | Enter command mode |
|
| : | Enter command mode |
|
||||||
| z | Enter view mode |
|
| z | Enter view mode |
|
||||||
|
| ctrl-w | Enter window mode (maybe will be remove for spc w w later) |
|
||||||
| space | Enter space mode |
|
| space | Enter space mode |
|
||||||
| K | Show documentation for the item under the cursor |
|
| K | Show documentation for the item under the cursor |
|
||||||
|
|
||||||
@@ -86,6 +86,18 @@ in reverse, or searching via smartcase.
|
|||||||
| N | Add next search match to selection |
|
| N | Add next search match to selection |
|
||||||
| * | Use current selection as the search pattern |
|
| * | Use current selection as the search pattern |
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
> NOTE: `[` and `]` will likely contain more pair mappings in the style of
|
||||||
|
> [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|-----|-----------|
|
||||||
|
| [d | Go to previous diagnostic |
|
||||||
|
| ]d | Go to next diagnostic |
|
||||||
|
| [D | Go to first diagnostic in document |
|
||||||
|
| ]D | Go to last diagnostic in document |
|
||||||
|
|
||||||
## Select / extend mode
|
## Select / extend mode
|
||||||
|
|
||||||
I'm still pondering whether to keep this mode or not. It changes movement
|
I'm still pondering whether to keep this mode or not. It changes movement
|
||||||
@@ -118,8 +130,13 @@ Jumps to various locations.
|
|||||||
|-----|-----------|
|
|-----|-----------|
|
||||||
| g | Go to the start of the file |
|
| g | Go to the start of the file |
|
||||||
| e | Go to the end of the file |
|
| e | Go to the end of the file |
|
||||||
|
| h | Go to the start of the line |
|
||||||
|
| l | Go to the end of the line |
|
||||||
|
| t | Go to the top of the screen |
|
||||||
|
| m | Go to the middle of the screen |
|
||||||
|
| b | Go to the bottom of the screen |
|
||||||
| d | Go to definition |
|
| d | Go to definition |
|
||||||
| t | Go to type definition |
|
| y | Go to type definition |
|
||||||
| r | Go to references |
|
| r | Go to references |
|
||||||
| i | Go to implementation |
|
| i | Go to implementation |
|
||||||
|
|
||||||
@@ -127,6 +144,17 @@ Jumps to various locations.
|
|||||||
|
|
||||||
TODO: Mappings for selecting syntax nodes (a superset of `[`).
|
TODO: Mappings for selecting syntax nodes (a superset of `[`).
|
||||||
|
|
||||||
|
## Window mode
|
||||||
|
|
||||||
|
This layer is similar to vim keybindings as kakoune does not support window.
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| w, ctrl-w | Switch to next window |
|
||||||
|
| v, ctrl-v | Vertical right split |
|
||||||
|
| h, ctrl-h | Horizontal bottom split |
|
||||||
|
| q, ctrl-q | Close current window |
|
||||||
|
|
||||||
## Space mode
|
## Space mode
|
||||||
|
|
||||||
This layer is a kludge of mappings I had under leader key in neovim.
|
This layer is a kludge of mappings I had under leader key in neovim.
|
||||||
@@ -135,7 +163,5 @@ This layer is a kludge of mappings I had under leader key in neovim.
|
|||||||
|-----|-----------|
|
|-----|-----------|
|
||||||
| f | Open file picker |
|
| f | Open file picker |
|
||||||
| b | Open buffer picker |
|
| b | Open buffer picker |
|
||||||
| v | Open a new vertical split into the current file |
|
| w | Enter window mode |
|
||||||
| w | Save changes to file |
|
|
||||||
| c | Close the current split |
|
|
||||||
| space | Keep primary selection TODO: it's here because space mode replaced it |
|
| space | Keep primary selection TODO: it's here because space mode replaced it |
|
||||||
|
9
contrib/themes/README.md
Normal file
9
contrib/themes/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# User submitted themes
|
||||||
|
|
||||||
|
If you submit a theme, please include a comment at the top with your name and email address:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Author : Name <email@my.domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
We have a preview page for themes on our [wiki](https://github.com/helix-editor/helix/wiki/Themes)!
|
46
contrib/themes/ingrid.toml
Normal file
46
contrib/themes/ingrid.toml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Author : Ingrid Rebecca Abraham <git@ingrids.email>
|
||||||
|
|
||||||
|
"attribute" = "#839A53"
|
||||||
|
"keyword" = { fg = "#D74E50", modifiers = ["bold"] }
|
||||||
|
"keyword.directive" = "#6F873E"
|
||||||
|
"namespace" = "#839A53"
|
||||||
|
"punctuation" = "#C97270"
|
||||||
|
"punctuation.delimiter" = "#C97270"
|
||||||
|
"operator" = { fg = "#D74E50", modifiers = ["bold"] }
|
||||||
|
"special" = "#D68482"
|
||||||
|
"property" = "#89BEB7"
|
||||||
|
"variable" = "#A6B6CE"
|
||||||
|
"variable.parameter" = "#89BEB7"
|
||||||
|
"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
|
||||||
|
"type.builtin" = "#839A53"
|
||||||
|
"constructor" = { fg = "#839A53", modifiers = ["bold"] }
|
||||||
|
"function" = { fg = "#89BEB7", modifiers = ["bold"] }
|
||||||
|
"function.macro" = { fg = "#D4A520", modifiers = ["bold"] }
|
||||||
|
"function.builtin" = "#89BEB7"
|
||||||
|
"comment" = "#A6B6CE"
|
||||||
|
"variable.builtin" = "#D4A520"
|
||||||
|
"constant" = "#D4A520"
|
||||||
|
"constant.builtin" = "#D4A520"
|
||||||
|
"string" = "#D74E50"
|
||||||
|
"number" = "#D74E50"
|
||||||
|
"escape" = { fg = "#D74E50", modifiers = ["bold"] }
|
||||||
|
"label" = "#D68482"
|
||||||
|
|
||||||
|
"module" = "#839A53"
|
||||||
|
|
||||||
|
"ui.background" = { bg = "#FFFCFD" }
|
||||||
|
"ui.linenr" = { fg = "#bbbbbb" }
|
||||||
|
"ui.statusline" = { bg = "#F3EAE9" }
|
||||||
|
"ui.popup" = { bg = "#F3EAE9" }
|
||||||
|
"ui.window" = { bg = "#D8B8B3" }
|
||||||
|
"ui.help" = { bg = "#D8B8B3", fg = "#250E07" }
|
||||||
|
|
||||||
|
"ui.text" = { fg = "#7B91B3" }
|
||||||
|
"ui.text.focus" = { fg = "#250E07", modifiers= ["bold"] }
|
||||||
|
|
||||||
|
"ui.menu.selected" = { fg = "#D74E50", bg = "#F3EAE9" }
|
||||||
|
|
||||||
|
"warning" = "#D4A520"
|
||||||
|
"error" = "#D74E50"
|
||||||
|
"info" = "#839A53"
|
||||||
|
"hint" = "#A6B6CE"
|
@@ -1,12 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helix-core"
|
name = "helix-core"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|
||||||
|
[features]
|
||||||
|
embed_runtime = ["rust-embed"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
helix-syntax = { path = "../helix-syntax" }
|
helix-syntax = { path = "../helix-syntax" }
|
||||||
|
|
||||||
@@ -24,3 +28,4 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
|
||||||
etcetera = "0.3"
|
etcetera = "0.3"
|
||||||
|
rust-embed = { version = "5.9.0", optional = true }
|
||||||
|
@@ -251,22 +251,25 @@ where
|
|||||||
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
let loader = Loader::new(Configuration {
|
let loader = Loader::new(
|
||||||
language: vec![LanguageConfiguration {
|
Configuration {
|
||||||
scope: "source.rust".to_string(),
|
language: vec![LanguageConfiguration {
|
||||||
file_types: vec!["rs".to_string()],
|
scope: "source.rust".to_string(),
|
||||||
language_id: Lang::Rust,
|
file_types: vec!["rs".to_string()],
|
||||||
highlight_config: OnceCell::new(),
|
language_id: Lang::Rust,
|
||||||
//
|
highlight_config: OnceCell::new(),
|
||||||
roots: vec![],
|
//
|
||||||
language_server: None,
|
roots: vec![],
|
||||||
indent: Some(IndentationConfiguration {
|
language_server: None,
|
||||||
tab_width: 4,
|
indent: Some(IndentationConfiguration {
|
||||||
unit: String::from(" "),
|
tab_width: 4,
|
||||||
}),
|
unit: String::from(" "),
|
||||||
indent_query: OnceCell::new(),
|
}),
|
||||||
}],
|
indent_query: OnceCell::new(),
|
||||||
});
|
}],
|
||||||
|
},
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
|
||||||
// set runtime path so we can find the queries
|
// set runtime path so we can find the queries
|
||||||
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
@@ -16,6 +16,7 @@ pub mod selection;
|
|||||||
mod state;
|
mod state;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
pub mod words;
|
||||||
|
|
||||||
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
|
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
|
||||||
// find first non-whitespace char
|
// find first non-whitespace char
|
||||||
@@ -44,6 +45,7 @@ pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(embed_runtime))]
|
||||||
pub fn runtime_dir() -> std::path::PathBuf {
|
pub fn runtime_dir() -> std::path::PathBuf {
|
||||||
// runtime env var || dir where binary is located
|
// runtime env var || dir where binary is located
|
||||||
std::env::var("HELIX_RUNTIME")
|
std::env::var("HELIX_RUNTIME")
|
||||||
@@ -58,13 +60,22 @@ pub fn runtime_dir() -> std::path::PathBuf {
|
|||||||
|
|
||||||
pub fn config_dir() -> std::path::PathBuf {
|
pub fn config_dir() -> std::path::PathBuf {
|
||||||
// TODO: allow env var override
|
// TODO: allow env var override
|
||||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
|
||||||
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
||||||
let mut path = strategy.config_dir();
|
let mut path = strategy.config_dir();
|
||||||
path.push("helix");
|
path.push("helix");
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cache_dir() -> std::path::PathBuf {
|
||||||
|
// TODO: allow env var override
|
||||||
|
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
||||||
|
let mut path = strategy.cache_dir();
|
||||||
|
path.push("helix");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||||
|
|
||||||
pub use ropey::{Rope, RopeSlice};
|
pub use ropey::{Rope, RopeSlice};
|
||||||
|
|
||||||
pub use tendril::StrTendril as Tendril;
|
pub use tendril::StrTendril as Tendril;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use crate::{Range, Rope, Selection, Syntax};
|
use crate::{Range, Rope, Selection, Syntax};
|
||||||
|
|
||||||
// const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']')];
|
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
|
||||||
// limit matching pairs to only ( ) { } [ ] < >
|
// limit matching pairs to only ( ) { } [ ] < >
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -20,15 +20,27 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
|
|||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let start_byte = node.start_byte();
|
if node.is_error() {
|
||||||
let end_byte = node.end_byte() - 1; // it's end exclusive
|
return None;
|
||||||
|
|
||||||
if start_byte == byte_pos {
|
|
||||||
return Some(doc.byte_to_char(end_byte));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if end_byte == byte_pos {
|
let start_byte = node.start_byte();
|
||||||
return Some(doc.byte_to_char(start_byte));
|
let len = doc.len_bytes();
|
||||||
|
if start_byte >= len {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let end_byte = node.end_byte() - 1; // it's end exclusive
|
||||||
|
let start_char = doc.byte_to_char(start_byte);
|
||||||
|
let end_char = doc.byte_to_char(end_byte);
|
||||||
|
|
||||||
|
if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
|
||||||
|
if start_byte == byte_pos {
|
||||||
|
return Some(end_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_byte == byte_pos {
|
||||||
|
return Some(start_char);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
@@ -45,7 +45,10 @@ pub fn move_vertically(
|
|||||||
|
|
||||||
let new_line = match dir {
|
let new_line = match dir {
|
||||||
Direction::Backward => row.saturating_sub(count),
|
Direction::Backward => row.saturating_sub(count),
|
||||||
Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 2),
|
Direction::Forward => std::cmp::min(
|
||||||
|
row.saturating_add(count),
|
||||||
|
text.len_lines().saturating_sub(2),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
|
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
|
||||||
@@ -171,22 +174,24 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
|
|||||||
|
|
||||||
// used for by-word movement
|
// used for by-word movement
|
||||||
|
|
||||||
fn is_word(ch: char) -> bool {
|
pub(crate) fn is_word(ch: char) -> bool {
|
||||||
ch.is_alphanumeric() || ch == '_'
|
ch.is_alphanumeric() || ch == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_horiz_blank(ch: char) -> bool {
|
pub(crate) fn is_horiz_blank(ch: char) -> bool {
|
||||||
matches!(ch, ' ' | '\t')
|
matches!(ch, ' ' | '\t')
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
enum Category {
|
pub(crate) enum Category {
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Eol,
|
Eol,
|
||||||
Word,
|
Word,
|
||||||
Punctuation,
|
Punctuation,
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
fn categorize(ch: char) -> Category {
|
|
||||||
|
pub(crate) fn categorize(ch: char) -> Category {
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
Category::Eol
|
Category::Eol
|
||||||
} else if ch.is_ascii_whitespace() {
|
} else if ch.is_ascii_whitespace() {
|
||||||
@@ -196,7 +201,7 @@ fn categorize(ch: char) -> Category {
|
|||||||
} else if ch.is_ascii_punctuation() {
|
} else if ch.is_ascii_punctuation() {
|
||||||
Category::Punctuation
|
Category::Punctuation
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
Category::Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@ pub fn find_nth_prev(
|
|||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
// start searching right before pos
|
// start searching right before pos
|
||||||
let mut chars = text.chars_at(pos.saturating_sub(1));
|
let mut chars = text.chars_at(pos);
|
||||||
|
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
loop {
|
loop {
|
||||||
@@ -56,7 +56,7 @@ pub fn find_nth_prev(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !inclusive {
|
if !inclusive {
|
||||||
pos -= 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(pos)
|
Some(pos)
|
||||||
|
@@ -383,7 +383,7 @@ pub fn split_on_matches(
|
|||||||
// TODO: retain range direction
|
// TODO: retain range direction
|
||||||
|
|
||||||
let end = text.byte_to_char(start_byte + mat.start());
|
let end = text.byte_to_char(start_byte + mat.start());
|
||||||
result.push(Range::new(start, end - 1));
|
result.push(Range::new(start, end.saturating_sub(1)));
|
||||||
start = text.byte_to_char(start_byte + mat.end());
|
start = text.byte_to_char(start_byte + mat.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -73,16 +73,46 @@ pub struct IndentQuery {
|
|||||||
pub outdent: HashSet<String>,
|
pub outdent: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "embed_runtime"))]
|
||||||
|
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||||
|
let root = crate::runtime_dir();
|
||||||
|
let path = root.join("queries").join(language).join(filename);
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "embed_runtime")]
|
||||||
|
fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(rust_embed::RustEmbed)]
|
||||||
|
#[folder = "../runtime/"]
|
||||||
|
struct Runtime;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct EmbeddedFileNotFoundError {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
impl std::error::Error for EmbeddedFileNotFoundError {}
|
||||||
|
impl fmt::Display for EmbeddedFileNotFoundError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "failed to load embedded file {}", self.path.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = PathBuf::from("queries").join(language).join(filename);
|
||||||
|
|
||||||
|
if let Some(query_bytes) = Runtime::get(&path.display().to_string()) {
|
||||||
|
String::from_utf8(query_bytes.to_vec()).map_err(|err| err.into())
|
||||||
|
} else {
|
||||||
|
Err(Box::new(EmbeddedFileNotFoundError { path }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_query(language: &str, filename: &str) -> String {
|
fn read_query(language: &str, filename: &str) -> String {
|
||||||
static INHERITS_REGEX: Lazy<Regex> =
|
static INHERITS_REGEX: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
|
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
|
||||||
|
|
||||||
let root = crate::runtime_dir();
|
let query = load_runtime_file(language, filename).unwrap_or_default();
|
||||||
// let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
|
|
||||||
let path = root.join("queries").join(language).join(filename);
|
|
||||||
|
|
||||||
let query = std::fs::read_to_string(&path).unwrap_or_default();
|
|
||||||
|
|
||||||
// TODO: the collect() is not ideal
|
// TODO: the collect() is not ideal
|
||||||
let inherits = INHERITS_REGEX
|
let inherits = INHERITS_REGEX
|
||||||
@@ -146,11 +176,8 @@ impl LanguageConfiguration {
|
|||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||||
|
|
||||||
let root = crate::runtime_dir();
|
let toml = load_runtime_file(&language, "indents.toml").ok()?;
|
||||||
let path = root.join("queries").join(language).join("indents.toml");
|
toml::from_slice(&toml.as_bytes()).ok()
|
||||||
|
|
||||||
let toml = std::fs::read(&path).ok()?;
|
|
||||||
toml::from_slice(&toml).ok()
|
|
||||||
})
|
})
|
||||||
.as_ref()
|
.as_ref()
|
||||||
}
|
}
|
||||||
@@ -166,13 +193,15 @@ pub struct Loader {
|
|||||||
// highlight_names ?
|
// highlight_names ?
|
||||||
language_configs: Vec<Arc<LanguageConfiguration>>,
|
language_configs: Vec<Arc<LanguageConfiguration>>,
|
||||||
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
|
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
|
||||||
|
scopes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Loader {
|
impl Loader {
|
||||||
pub fn new(config: Configuration) -> Self {
|
pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
|
||||||
let mut loader = Self {
|
let mut loader = Self {
|
||||||
language_configs: Vec::new(),
|
language_configs: Vec::new(),
|
||||||
language_config_ids_by_file_type: HashMap::new(),
|
language_config_ids_by_file_type: HashMap::new(),
|
||||||
|
scopes,
|
||||||
};
|
};
|
||||||
|
|
||||||
for config in config.language {
|
for config in config.language {
|
||||||
@@ -192,6 +221,10 @@ impl Loader {
|
|||||||
loader
|
loader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scopes(&self) -> &[String] {
|
||||||
|
&self.scopes
|
||||||
|
}
|
||||||
|
|
||||||
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
|
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
|
||||||
// Find all the language configurations that match this file name
|
// Find all the language configurations that match this file name
|
||||||
// or a suffix of the file name.
|
// or a suffix of the file name.
|
||||||
@@ -1700,3 +1733,13 @@ fn test_input_edits() {
|
|||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_runtime_file() {
|
||||||
|
// Test to make sure we can load some data from the runtime directory.
|
||||||
|
let contents = load_runtime_file("rust", "indents.toml").unwrap();
|
||||||
|
assert!(!contents.is_empty());
|
||||||
|
|
||||||
|
let results = load_runtime_file("rust", "does-not-exist");
|
||||||
|
assert!(results.is_err());
|
||||||
|
}
|
||||||
|
@@ -90,7 +90,8 @@ impl ChangeSet {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.len_after += fragment.len();
|
// Avoiding std::str::len() to account for UTF-8 characters.
|
||||||
|
self.len_after += fragment.chars().count();
|
||||||
|
|
||||||
let new_last = match self.changes.as_mut_slice() {
|
let new_last = match self.changes.as_mut_slice() {
|
||||||
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
|
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
|
||||||
@@ -754,4 +755,21 @@ mod test {
|
|||||||
use Operation::*;
|
use Operation::*;
|
||||||
assert_eq!(changes.changes, &[Insert("a".into())]);
|
assert_eq!(changes.changes, &[Insert("a".into())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_with_utf8() {
|
||||||
|
const TEST_CASE: &'static str = "Hello, これはヒレクスエディターです!";
|
||||||
|
|
||||||
|
let empty = Rope::from("");
|
||||||
|
let mut a = ChangeSet::new(&empty);
|
||||||
|
|
||||||
|
let mut b = ChangeSet::new(&empty);
|
||||||
|
b.insert(TEST_CASE.into());
|
||||||
|
|
||||||
|
let changes = a.compose(b);
|
||||||
|
|
||||||
|
use Operation::*;
|
||||||
|
assert_eq!(changes.changes, &[Insert(TEST_CASE.into())]);
|
||||||
|
assert_eq!(changes.len_after, TEST_CASE.chars().count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
65
helix-core/src/words.rs
Normal file
65
helix-core/src/words.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::movement::{categorize, is_horiz_blank, is_word, skip_over_prev};
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn nth_prev_word_boundary(slice: RopeSlice, mut char_idx: usize, count: usize) -> usize {
|
||||||
|
let mut with_end = false;
|
||||||
|
|
||||||
|
for _ in 0..count {
|
||||||
|
if char_idx == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return if not skip while?
|
||||||
|
skip_over_prev(slice, &mut char_idx, |ch| ch == '\n');
|
||||||
|
|
||||||
|
with_end = skip_over_prev(slice, &mut char_idx, is_horiz_blank);
|
||||||
|
|
||||||
|
// refetch
|
||||||
|
let ch = slice.char(char_idx);
|
||||||
|
|
||||||
|
if is_word(ch) {
|
||||||
|
with_end = skip_over_prev(slice, &mut char_idx, is_word);
|
||||||
|
} else if ch.is_ascii_punctuation() {
|
||||||
|
with_end = skip_over_prev(slice, &mut char_idx, |ch| ch.is_ascii_punctuation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if with_end {
|
||||||
|
char_idx
|
||||||
|
} else {
|
||||||
|
char_idx + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_prev_word_boundary() {
|
||||||
|
use ropey::Rope;
|
||||||
|
let t = |x, y| {
|
||||||
|
let text = Rope::from(x);
|
||||||
|
let out = nth_prev_word_boundary(text.slice(..), text.len_chars() - 1, 1);
|
||||||
|
assert_eq!(text.slice(..out), y, r#"from "{}""#, x);
|
||||||
|
};
|
||||||
|
t("abcd\nefg\nwrs", "abcd\nefg\n");
|
||||||
|
t("abcd\nefg\n", "abcd\n");
|
||||||
|
t("abcd\n", "");
|
||||||
|
t("hello, world!", "hello, world");
|
||||||
|
t("hello, world", "hello, ");
|
||||||
|
t("hello, ", "hello");
|
||||||
|
t("hello", "");
|
||||||
|
t("こんにちは、世界!", "こんにちは、世界!"); // TODO: punctuation
|
||||||
|
t("こんにちは、世界", "こんにちは、");
|
||||||
|
t("こんにちは、", "こんにちは、"); // what?
|
||||||
|
t("こんにちは", "");
|
||||||
|
t("この世界。", "この世界。"); // what?
|
||||||
|
t("この世界", "");
|
||||||
|
t("お前はもう死んでいる", "");
|
||||||
|
t("その300円です", ""); // TODO: should stop at 300
|
||||||
|
t("唱k", ""); // TODO: should stop at 唱
|
||||||
|
t("1 + 1 = 2", "1 + 1 = ");
|
||||||
|
t("1 + 1 =", "1 + 1 ");
|
||||||
|
t("1 + 1", "1 + ");
|
||||||
|
t("1 + ", "1 ");
|
||||||
|
t("1 ", "");
|
||||||
|
t("1+1=2", "1+1=");
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helix-lsp"
|
name = "helix-lsp"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
mod client;
|
mod client;
|
||||||
mod select_all;
|
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
pub use jsonrpc_core as jsonrpc;
|
pub use jsonrpc_core as jsonrpc;
|
||||||
@@ -171,7 +170,7 @@ pub use jsonrpc::Call;
|
|||||||
|
|
||||||
type LanguageId = String;
|
type LanguageId = String;
|
||||||
|
|
||||||
use crate::select_all::SelectAll;
|
use futures_util::stream::select_all::SelectAll;
|
||||||
|
|
||||||
pub struct Registry {
|
pub struct Registry {
|
||||||
inner: HashMap<LanguageId, Option<Arc<Client>>>,
|
inner: HashMap<LanguageId, Option<Arc<Client>>>,
|
||||||
@@ -198,7 +197,7 @@ impl Registry {
|
|||||||
if let Some(config) = &language_config.language_server {
|
if let Some(config) = &language_config.language_server {
|
||||||
// avoid borrow issues
|
// avoid borrow issues
|
||||||
let inner = &mut self.inner;
|
let inner = &mut self.inner;
|
||||||
let s_incoming = &self.incoming;
|
let s_incoming = &mut self.incoming;
|
||||||
|
|
||||||
let language_server = inner
|
let language_server = inner
|
||||||
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
|
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
|
||||||
|
@@ -1,143 +0,0 @@
|
|||||||
//! An unbounded set of streams
|
|
||||||
|
|
||||||
use core::{
|
|
||||||
fmt::{self, Debug},
|
|
||||||
iter::FromIterator,
|
|
||||||
pin::Pin,
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use futures_util::stream::{FusedStream, FuturesUnordered, StreamExt, StreamFuture};
|
|
||||||
use futures_util::{ready, stream::Stream};
|
|
||||||
|
|
||||||
/// An unbounded set of streams
|
|
||||||
///
|
|
||||||
/// This "combinator" provides the ability to maintain a set of streams
|
|
||||||
/// and drive them all to completion.
|
|
||||||
///
|
|
||||||
/// Streams are pushed into this set and their realized values are
|
|
||||||
/// yielded as they become ready. Streams will only be polled when they
|
|
||||||
/// generate notifications. This allows to coordinate a large number of streams.
|
|
||||||
///
|
|
||||||
/// Note that you can create a ready-made `SelectAll` via the
|
|
||||||
/// `select_all` function in the `stream` module, or you can start with an
|
|
||||||
/// empty set with the `SelectAll::new` constructor.
|
|
||||||
#[must_use = "streams do nothing unless polled"]
|
|
||||||
pub struct SelectAll<St> {
|
|
||||||
inner: FuturesUnordered<StreamFuture<St>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Debug> Debug for SelectAll<St> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "SelectAll {{ ... }}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Stream + Unpin> SelectAll<St> {
|
|
||||||
/// Constructs a new, empty `SelectAll`
|
|
||||||
///
|
|
||||||
/// The returned `SelectAll` does not contain any streams and, in this
|
|
||||||
/// state, `SelectAll::poll` will return `Poll::Ready(None)`.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: FuturesUnordered::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of streams contained in the set.
|
|
||||||
///
|
|
||||||
/// This represents the total number of in-flight streams.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.inner.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if the set contains no streams
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.inner.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a stream into the set.
|
|
||||||
///
|
|
||||||
/// This function submits the given stream to the set for managing. This
|
|
||||||
/// function will not call `poll` on the submitted stream. The caller must
|
|
||||||
/// ensure that `SelectAll::poll` is called in order to receive task
|
|
||||||
/// notifications.
|
|
||||||
pub fn push(&self, stream: St) {
|
|
||||||
self.inner.push(stream.into_future());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Stream + Unpin> Default for SelectAll<St> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Stream + Unpin> Stream for SelectAll<St> {
|
|
||||||
type Item = St::Item;
|
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
||||||
loop {
|
|
||||||
match ready!(self.inner.poll_next_unpin(cx)) {
|
|
||||||
Some((Some(item), remaining)) => {
|
|
||||||
self.push(remaining);
|
|
||||||
return Poll::Ready(Some(item));
|
|
||||||
}
|
|
||||||
Some((None, _)) => {
|
|
||||||
// `FuturesUnordered` thinks it isn't terminated
|
|
||||||
// because it yielded a Some.
|
|
||||||
// We do not return, but poll `FuturesUnordered`
|
|
||||||
// in the next loop iteration.
|
|
||||||
}
|
|
||||||
None => return Poll::Ready(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Stream + Unpin> FusedStream for SelectAll<St> {
|
|
||||||
fn is_terminated(&self) -> bool {
|
|
||||||
self.inner.is_terminated()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a list of streams into a `Stream` of results from the streams.
|
|
||||||
///
|
|
||||||
/// This essentially takes a list of streams (e.g. a vector, an iterator, etc.)
|
|
||||||
/// and bundles them together into a single stream.
|
|
||||||
/// The stream will yield items as they become available on the underlying
|
|
||||||
/// streams internally, in the order they become available.
|
|
||||||
///
|
|
||||||
/// Note that the returned set can also be used to dynamically push more
|
|
||||||
/// futures into the set as they become available.
|
|
||||||
///
|
|
||||||
/// This function is only available when the `std` or `alloc` feature of this
|
|
||||||
/// library is activated, and it is activated by default.
|
|
||||||
pub fn select_all<I>(streams: I) -> SelectAll<I::Item>
|
|
||||||
where
|
|
||||||
I: IntoIterator,
|
|
||||||
I::Item: Stream + Unpin,
|
|
||||||
{
|
|
||||||
let set = SelectAll::new();
|
|
||||||
|
|
||||||
for stream in streams {
|
|
||||||
set.push(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Stream + Unpin> FromIterator<St> for SelectAll<St> {
|
|
||||||
fn from_iter<T: IntoIterator<Item = St>>(iter: T) -> Self {
|
|
||||||
select_all(iter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<St: Stream + Unpin> Extend<St> for SelectAll<St> {
|
|
||||||
fn extend<T: IntoIterator<Item = St>>(&mut self, iter: T) {
|
|
||||||
for st in iter {
|
|
||||||
self.push(st)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helix-syntax"
|
name = "helix-syntax"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
@@ -1,16 +1,8 @@
|
|||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{env, fs};
|
|
||||||
|
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
fn get_opt_level() -> u32 {
|
|
||||||
env::var("OPT_LEVEL").unwrap().parse::<u32>().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_debug() -> bool {
|
|
||||||
env::var("DEBUG").unwrap() == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
|
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
|
||||||
let mut dirs = Vec::new();
|
let mut dirs = Vec::new();
|
||||||
for entry in fs::read_dir("languages").unwrap().flatten() {
|
for entry in fs::read_dir("languages").unwrap().flatten() {
|
||||||
@@ -58,25 +50,28 @@ fn build_c(files: Vec<String>, language: &str) {
|
|||||||
.file(&file)
|
.file(&file)
|
||||||
.include(PathBuf::from(file).parent().unwrap())
|
.include(PathBuf::from(file).parent().unwrap())
|
||||||
.pic(true)
|
.pic(true)
|
||||||
.opt_level(get_opt_level())
|
.warnings(false);
|
||||||
.debug(get_debug())
|
|
||||||
.warnings(false)
|
|
||||||
.flag_if_supported("-std=c99");
|
|
||||||
}
|
}
|
||||||
build.compile(&format!("tree-sitter-{}-c", language));
|
build.compile(&format!("tree-sitter-{}-c", language));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_cpp(files: Vec<String>, language: &str) {
|
fn build_cpp(files: Vec<String>, language: &str) {
|
||||||
let mut build = cc::Build::new();
|
let mut build = cc::Build::new();
|
||||||
|
|
||||||
|
let flag = if build.get_compiler().is_like_msvc() {
|
||||||
|
"/std:c++17"
|
||||||
|
} else {
|
||||||
|
"-std=c++14"
|
||||||
|
};
|
||||||
|
|
||||||
for file in files {
|
for file in files {
|
||||||
build
|
build
|
||||||
.file(&file)
|
.file(&file)
|
||||||
.include(PathBuf::from(file).parent().unwrap())
|
.include(PathBuf::from(file).parent().unwrap())
|
||||||
.pic(true)
|
.pic(true)
|
||||||
.opt_level(get_opt_level())
|
|
||||||
.debug(get_debug())
|
|
||||||
.warnings(false)
|
.warnings(false)
|
||||||
.cpp(true);
|
.cpp(true)
|
||||||
|
.flag_if_supported(flag);
|
||||||
}
|
}
|
||||||
build.compile(&format!("tree-sitter-{}-cpp", language));
|
build.compile(&format!("tree-sitter-{}-cpp", language));
|
||||||
}
|
}
|
||||||
@@ -109,6 +104,7 @@ fn build_dir(dir: &str, language: &str) {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let ignore = vec![
|
let ignore = vec![
|
||||||
"tree-sitter-typescript".to_string(),
|
"tree-sitter-typescript".to_string(),
|
||||||
|
"tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
|
||||||
".DS_Store".to_string(),
|
".DS_Store".to_string(),
|
||||||
];
|
];
|
||||||
let dirs = collect_tree_sitter_dirs(&ignore);
|
let dirs = collect_tree_sitter_dirs(&ignore);
|
||||||
|
1
helix-syntax/languages/tree-sitter-haskell
Submodule
1
helix-syntax/languages/tree-sitter-haskell
Submodule
Submodule helix-syntax/languages/tree-sitter-haskell added at 237f4eb441
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helix-term"
|
name = "helix-term"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
description = "A post-modern text editor."
|
description = "A post-modern text editor."
|
||||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -8,6 +8,9 @@ license = "MPL-2.0"
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[features]
|
||||||
|
embed_runtime = ["helix-core/embed_runtime"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "hx"
|
name = "hx"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
@@ -45,16 +45,28 @@ impl Application {
|
|||||||
let size = compositor.size();
|
let size = compositor.size();
|
||||||
let mut editor = Editor::new(size);
|
let mut editor = Editor::new(size);
|
||||||
|
|
||||||
|
compositor.push(Box::new(ui::EditorView::new()));
|
||||||
|
|
||||||
if !args.files.is_empty() {
|
if !args.files.is_empty() {
|
||||||
for file in args.files {
|
let first = &args.files[0]; // we know it's not empty
|
||||||
editor.open(file, Action::VerticalSplit)?;
|
if first.is_dir() {
|
||||||
|
editor.new_file(Action::VerticalSplit);
|
||||||
|
compositor.push(Box::new(ui::file_picker(first.clone())));
|
||||||
|
} else {
|
||||||
|
for file in args.files {
|
||||||
|
if file.is_dir() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
editor.open(file, Action::VerticalSplit)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
editor.new_file(Action::VerticalSplit);
|
editor.new_file(Action::VerticalSplit);
|
||||||
}
|
}
|
||||||
|
|
||||||
compositor.push(Box::new(ui::EditorView::new()));
|
|
||||||
|
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
compositor,
|
compositor,
|
||||||
editor,
|
editor,
|
||||||
@@ -205,7 +217,7 @@ impl Application {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
doc.diagnostics = diagnostics;
|
doc.set_diagnostics(diagnostics);
|
||||||
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
|
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
|
||||||
self.render();
|
self.render();
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,8 @@ use helix_core::{
|
|||||||
movement::{self, Direction},
|
movement::{self, Direction},
|
||||||
object, pos_at_coords,
|
object, pos_at_coords,
|
||||||
regex::{self, Regex},
|
regex::{self, Regex},
|
||||||
register, search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection,
|
register, search, selection, words, Change, ChangeSet, Position, Range, Rope, RopeSlice,
|
||||||
SmallVec, Tendril, Transaction,
|
Selection, SmallVec, Tendril, Transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
@@ -315,7 +315,7 @@ fn _find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
|
|||||||
where
|
where
|
||||||
// TODO: make an options struct for and abstract this Fn into a searcher type
|
// TODO: make an options struct for and abstract this Fn into a searcher type
|
||||||
// use the definition for w/b/e too
|
// use the definition for w/b/e too
|
||||||
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize>,
|
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
|
||||||
{
|
{
|
||||||
// TODO: count is reset to 1 before next key so we move it into the closure here.
|
// TODO: count is reset to 1 before next key so we move it into the closure here.
|
||||||
// Would be nice to carry over.
|
// Would be nice to carry over.
|
||||||
@@ -332,7 +332,7 @@ where
|
|||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
|
|
||||||
let selection = doc.selection(view.id).transform(|mut range| {
|
let selection = doc.selection(view.id).transform(|mut range| {
|
||||||
search::find_nth_next(text, ch, range.head, count, inclusive).map_or(range, |pos| {
|
search_fn(text, ch, range.head, count, inclusive).map_or(range, |pos| {
|
||||||
if extend {
|
if extend {
|
||||||
Range::new(range.anchor, pos)
|
Range::new(range.anchor, pos)
|
||||||
} else {
|
} else {
|
||||||
@@ -475,8 +475,8 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
|
|||||||
// clamp into viewport
|
// clamp into viewport
|
||||||
let line = cursor
|
let line = cursor
|
||||||
.row
|
.row
|
||||||
.min(view.first_line + scrolloff)
|
.max(view.first_line + scrolloff)
|
||||||
.max(last_line.saturating_sub(scrolloff));
|
.min(last_line.saturating_sub(scrolloff));
|
||||||
|
|
||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
|
let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
|
||||||
@@ -573,6 +573,37 @@ pub fn extend_line_down(cx: &mut Context) {
|
|||||||
doc.set_selection(view.id, selection);
|
doc.set_selection(view.id, selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extend_line_end(cx: &mut Context) {
|
||||||
|
let (view, doc) = cx.current();
|
||||||
|
|
||||||
|
let selection = doc.selection(view.id).transform(|range| {
|
||||||
|
let text = doc.text();
|
||||||
|
let line = text.char_to_line(range.head);
|
||||||
|
|
||||||
|
// Line end is pos at the start of next line - 1
|
||||||
|
// subtract another 1 because the line ends with \n
|
||||||
|
let pos = text.line_to_char(line + 1).saturating_sub(2);
|
||||||
|
Range::new(range.anchor, pos)
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.set_selection(view.id, selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_line_start(cx: &mut Context) {
|
||||||
|
let (view, doc) = cx.current();
|
||||||
|
|
||||||
|
let selection = doc.selection(view.id).transform(|range| {
|
||||||
|
let text = doc.text();
|
||||||
|
let line = text.char_to_line(range.head);
|
||||||
|
|
||||||
|
// adjust to start of the line
|
||||||
|
let pos = text.line_to_char(line);
|
||||||
|
Range::new(range.anchor, pos)
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.set_selection(view.id, selection);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_all(cx: &mut Context) {
|
pub fn select_all(cx: &mut Context) {
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
|
|
||||||
@@ -638,7 +669,7 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let head = end - 1;
|
let head = end;
|
||||||
|
|
||||||
let selection = if extend {
|
let selection = if extend {
|
||||||
selection.clone().push(Range::new(start, head))
|
selection.clone().push(Range::new(start, head))
|
||||||
@@ -718,7 +749,9 @@ pub fn select_line(cx: &mut Context) {
|
|||||||
|
|
||||||
let line = text.char_to_line(pos.head);
|
let line = text.char_to_line(pos.head);
|
||||||
let start = text.line_to_char(line);
|
let start = text.line_to_char(line);
|
||||||
let end = text.line_to_char(line + count).saturating_sub(1);
|
let end = text
|
||||||
|
.line_to_char(std::cmp::min(doc.text().len_lines(), line + count))
|
||||||
|
.saturating_sub(1);
|
||||||
|
|
||||||
doc.set_selection(view.id, Selection::single(start, end));
|
doc.set_selection(view.id, Selection::single(start, end));
|
||||||
}
|
}
|
||||||
@@ -759,7 +792,9 @@ fn _delete_selection(doc: &mut Document, view_id: ViewId) {
|
|||||||
// then delete
|
// then delete
|
||||||
let transaction =
|
let transaction =
|
||||||
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
|
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
|
||||||
(range.from(), range.to() + 1, None)
|
let max_to = doc.text().len_chars().saturating_sub(1);
|
||||||
|
let to = std::cmp::min(max_to, range.to() + 1);
|
||||||
|
(range.from(), to, None)
|
||||||
});
|
});
|
||||||
doc.apply(&transaction, view_id);
|
doc.apply(&transaction, view_id);
|
||||||
}
|
}
|
||||||
@@ -769,6 +804,9 @@ pub fn delete_selection(cx: &mut Context) {
|
|||||||
_delete_selection(doc, view.id);
|
_delete_selection(doc, view.id);
|
||||||
|
|
||||||
doc.append_changes_to_history(view.id);
|
doc.append_changes_to_history(view.id);
|
||||||
|
|
||||||
|
// exit select mode, if currently in select mode
|
||||||
|
exit_select_mode(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn change_selection(cx: &mut Context) {
|
pub fn change_selection(cx: &mut Context) {
|
||||||
@@ -1182,7 +1220,7 @@ fn open(cx: &mut Context, open: Open) {
|
|||||||
let text = text.repeat(count);
|
let text = text.repeat(count);
|
||||||
|
|
||||||
// calculate new selection range
|
// calculate new selection range
|
||||||
let pos = index + text.len();
|
let pos = index + text.chars().count();
|
||||||
ranges.push(Range::new(pos, pos));
|
ranges.push(Range::new(pos, pos));
|
||||||
|
|
||||||
(index, index, Some(text.into()))
|
(index, index, Some(text.into()))
|
||||||
@@ -1248,7 +1286,8 @@ pub fn goto_mode(cx: &mut Context) {
|
|||||||
// TODO: can't go to line 1 since we can't distinguish between g and 1g, g gets converted
|
// TODO: can't go to line 1 since we can't distinguish between g and 1g, g gets converted
|
||||||
// to 1g
|
// to 1g
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
let pos = doc.text().line_to_char(count - 1);
|
let line_idx = std::cmp::min(count - 1, doc.text().len_lines().saturating_sub(2));
|
||||||
|
let pos = doc.text().line_to_char(line_idx);
|
||||||
doc.set_selection(view.id, Selection::point(pos));
|
doc.set_selection(view.id, Selection::point(pos));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1263,10 +1302,35 @@ pub fn goto_mode(cx: &mut Context) {
|
|||||||
match ch {
|
match ch {
|
||||||
'g' => move_file_start(cx),
|
'g' => move_file_start(cx),
|
||||||
'e' => move_file_end(cx),
|
'e' => move_file_end(cx),
|
||||||
|
'h' => move_line_start(cx),
|
||||||
|
'l' => move_line_end(cx),
|
||||||
'd' => goto_definition(cx),
|
'd' => goto_definition(cx),
|
||||||
't' => goto_type_definition(cx),
|
'y' => goto_type_definition(cx),
|
||||||
'r' => goto_reference(cx),
|
'r' => goto_reference(cx),
|
||||||
'i' => goto_implementation(cx),
|
'i' => goto_implementation(cx),
|
||||||
|
|
||||||
|
't' | 'm' | 'b' => {
|
||||||
|
let (view, doc) = cx.current();
|
||||||
|
|
||||||
|
let pos = doc.selection(view.id).cursor();
|
||||||
|
let line = doc.text().char_to_line(pos);
|
||||||
|
|
||||||
|
let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref
|
||||||
|
|
||||||
|
let last_line = view.last_line(doc);
|
||||||
|
|
||||||
|
let line = match ch {
|
||||||
|
't' => (view.first_line + scrolloff),
|
||||||
|
'm' => (view.first_line + (view.area.height as usize / 2)),
|
||||||
|
'b' => last_line.saturating_sub(scrolloff),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
.min(last_line.saturating_sub(scrolloff));
|
||||||
|
|
||||||
|
let pos = doc.text().line_to_char(line);
|
||||||
|
|
||||||
|
doc.set_selection(view.id, Selection::point(pos));
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1472,6 +1536,86 @@ pub fn goto_reference(cx: &mut Context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn goto_pos(editor: &mut Editor, pos: usize) {
|
||||||
|
push_jump(editor);
|
||||||
|
|
||||||
|
let (view, doc) = editor.current();
|
||||||
|
|
||||||
|
doc.set_selection(view.id, Selection::point(pos));
|
||||||
|
align_view(doc, view, Align::Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_first_diag(cx: &mut Context) {
|
||||||
|
let editor = &mut cx.editor;
|
||||||
|
let (view, doc) = editor.current();
|
||||||
|
|
||||||
|
let cursor_pos = doc.selection(view.id).cursor();
|
||||||
|
let diag = if let Some(diag) = doc.diagnostics().first() {
|
||||||
|
diag.range.start
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_pos(editor, diag);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_last_diag(cx: &mut Context) {
|
||||||
|
let editor = &mut cx.editor;
|
||||||
|
let (view, doc) = editor.current();
|
||||||
|
|
||||||
|
let cursor_pos = doc.selection(view.id).cursor();
|
||||||
|
let diag = if let Some(diag) = doc.diagnostics().last() {
|
||||||
|
diag.range.start
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_pos(editor, diag);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_next_diag(cx: &mut Context) {
|
||||||
|
let editor = &mut cx.editor;
|
||||||
|
let (view, doc) = editor.current();
|
||||||
|
|
||||||
|
let cursor_pos = doc.selection(view.id).cursor();
|
||||||
|
let diag = if let Some(diag) = doc
|
||||||
|
.diagnostics()
|
||||||
|
.iter()
|
||||||
|
.map(|diag| diag.range.start)
|
||||||
|
.find(|&pos| pos > cursor_pos)
|
||||||
|
{
|
||||||
|
diag
|
||||||
|
} else if let Some(diag) = doc.diagnostics().first() {
|
||||||
|
diag.range.start
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_pos(editor, diag);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_prev_diag(cx: &mut Context) {
|
||||||
|
let editor = &mut cx.editor;
|
||||||
|
let (view, doc) = editor.current();
|
||||||
|
|
||||||
|
let cursor_pos = doc.selection(view.id).cursor();
|
||||||
|
let diag = if let Some(diag) = doc
|
||||||
|
.diagnostics()
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.map(|diag| diag.range.start)
|
||||||
|
.find(|&pos| pos < cursor_pos)
|
||||||
|
{
|
||||||
|
diag
|
||||||
|
} else if let Some(diag) = doc.diagnostics().last() {
|
||||||
|
diag.range.start
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
goto_pos(editor, diag);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn signature_help(cx: &mut Context) {
|
pub fn signature_help(cx: &mut Context) {
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
|
|
||||||
@@ -1667,7 +1811,7 @@ pub mod insert {
|
|||||||
text.push('\n');
|
text.push('\n');
|
||||||
text.push_str(&indent);
|
text.push_str(&indent);
|
||||||
|
|
||||||
let head = pos + offs + text.len();
|
let head = pos + offs + text.chars().count();
|
||||||
|
|
||||||
// TODO: range replace or extend
|
// TODO: range replace or extend
|
||||||
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
|
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
|
||||||
@@ -1689,7 +1833,7 @@ pub mod insert {
|
|||||||
text.push_str(&indent);
|
text.push_str(&indent);
|
||||||
}
|
}
|
||||||
|
|
||||||
offs += text.len();
|
offs += text.chars().count();
|
||||||
|
|
||||||
(pos, pos, Some(text.into()))
|
(pos, pos, Some(text.into()))
|
||||||
});
|
});
|
||||||
@@ -1718,7 +1862,6 @@ pub mod insert {
|
|||||||
|
|
||||||
pub fn delete_char_forward(cx: &mut Context) {
|
pub fn delete_char_forward(cx: &mut Context) {
|
||||||
let count = cx.count;
|
let count = cx.count;
|
||||||
let doc = cx.doc();
|
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
let transaction =
|
let transaction =
|
||||||
@@ -1731,6 +1874,21 @@ pub mod insert {
|
|||||||
});
|
});
|
||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_word_backward(cx: &mut Context) {
|
||||||
|
let count = cx.count;
|
||||||
|
let (view, doc) = cx.current();
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
let transaction =
|
||||||
|
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
|
||||||
|
(
|
||||||
|
words::nth_prev_word_boundary(text, range.head, count),
|
||||||
|
range.head,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
doc.apply(&transaction, view.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undo / Redo
|
// Undo / Redo
|
||||||
@@ -2176,11 +2334,6 @@ pub fn hover(cx: &mut Context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// view movements
|
|
||||||
pub fn next_view(cx: &mut Context) {
|
|
||||||
cx.editor.focus_next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// comments
|
// comments
|
||||||
pub fn toggle_comments(cx: &mut Context) {
|
pub fn toggle_comments(cx: &mut Context) {
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
@@ -2244,16 +2397,38 @@ pub fn jump_backward(cx: &mut Context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
pub fn window_mode(cx: &mut Context) {
|
||||||
|
cx.on_next_key(move |cx, event| {
|
||||||
|
if let KeyEvent {
|
||||||
|
code: KeyCode::Char(ch),
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
match ch {
|
||||||
|
'w' => rotate_view(cx),
|
||||||
|
'h' => hsplit(cx),
|
||||||
|
'v' => vsplit(cx),
|
||||||
|
'q' => wclose(cx),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn vsplit(cx: &mut Context) {
|
pub fn rotate_view(cx: &mut Context) {
|
||||||
|
cx.editor.focus_next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// split helper, clear it later
|
||||||
|
use helix_view::editor::Action;
|
||||||
|
fn split(cx: &mut Context, action: Action) {
|
||||||
use helix_view::editor::Action;
|
use helix_view::editor::Action;
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
let id = doc.id();
|
let id = doc.id();
|
||||||
let selection = doc.selection(view.id).clone();
|
let selection = doc.selection(view.id).clone();
|
||||||
let first_line = view.first_line;
|
let first_line = view.first_line;
|
||||||
|
|
||||||
cx.editor.switch(id, Action::VerticalSplit);
|
cx.editor.switch(id, action);
|
||||||
|
|
||||||
// match the selection in the previous view
|
// match the selection in the previous view
|
||||||
let (view, doc) = cx.current();
|
let (view, doc) = cx.current();
|
||||||
@@ -2261,6 +2436,20 @@ pub fn vsplit(cx: &mut Context) {
|
|||||||
doc.set_selection(view.id, selection);
|
doc.set_selection(view.id, selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hsplit(cx: &mut Context) {
|
||||||
|
split(cx, Action::HorizontalSplit);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vsplit(cx: &mut Context) {
|
||||||
|
split(cx, Action::VerticalSplit);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wclose(cx: &mut Context) {
|
||||||
|
let view_id = cx.view().id;
|
||||||
|
// close current split
|
||||||
|
cx.editor.close(view_id, /* close_buffer */ false);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn space_mode(cx: &mut Context) {
|
pub fn space_mode(cx: &mut Context) {
|
||||||
cx.on_next_key(move |cx, event| {
|
cx.on_next_key(move |cx, event| {
|
||||||
if let KeyEvent {
|
if let KeyEvent {
|
||||||
@@ -2272,18 +2461,7 @@ pub fn space_mode(cx: &mut Context) {
|
|||||||
match ch {
|
match ch {
|
||||||
'f' => file_picker(cx),
|
'f' => file_picker(cx),
|
||||||
'b' => buffer_picker(cx),
|
'b' => buffer_picker(cx),
|
||||||
'v' => vsplit(cx),
|
'w' => window_mode(cx),
|
||||||
'w' => {
|
|
||||||
// save current buffer
|
|
||||||
let (view, doc) = cx.current();
|
|
||||||
doc.format(view.id); // TODO: merge into save
|
|
||||||
tokio::spawn(doc.save());
|
|
||||||
}
|
|
||||||
'c' => {
|
|
||||||
let view_id = cx.view().id;
|
|
||||||
// close current split
|
|
||||||
cx.editor.close(view_id, /* close_buffer */ false);
|
|
||||||
}
|
|
||||||
// ' ' => toggle_alternate_buffer(cx),
|
// ' ' => toggle_alternate_buffer(cx),
|
||||||
// TODO: temporary since space mode took it's old key
|
// TODO: temporary since space mode took it's old key
|
||||||
' ' => keep_primary_selection(cx),
|
' ' => keep_primary_selection(cx),
|
||||||
@@ -2324,7 +2502,7 @@ pub fn view_mode(cx: &mut Context) {
|
|||||||
let pos = coords_at_pos(doc.text().slice(..), pos);
|
let pos = coords_at_pos(doc.text().slice(..), pos);
|
||||||
|
|
||||||
const OFFSET: usize = 7; // gutters
|
const OFFSET: usize = 7; // gutters
|
||||||
view.first_col = pos.col.saturating_sub((view.area.width as usize - OFFSET) / 2);
|
view.first_col = pos.col.saturating_sub(((view.area.width as usize).saturating_sub(OFFSET)) / 2);
|
||||||
},
|
},
|
||||||
'h' => (),
|
'h' => (),
|
||||||
'j' => scroll(cx, 1, Direction::Forward),
|
'j' => scroll(cx, 1, Direction::Forward),
|
||||||
@@ -2335,3 +2513,35 @@ pub fn view_mode(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn left_bracket_mode(cx: &mut Context) {
|
||||||
|
cx.on_next_key(move |cx, event| {
|
||||||
|
if let KeyEvent {
|
||||||
|
code: KeyCode::Char(ch),
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
match ch {
|
||||||
|
'd' => goto_prev_diag(cx),
|
||||||
|
'D' => goto_first_diag(cx),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_bracket_mode(cx: &mut Context) {
|
||||||
|
cx.on_next_key(move |cx, event| {
|
||||||
|
if let KeyEvent {
|
||||||
|
code: KeyCode::Char(ch),
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
match ch {
|
||||||
|
'd' => goto_next_diag(cx),
|
||||||
|
'D' => goto_last_diag(cx),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -35,10 +35,10 @@ use std::collections::HashMap;
|
|||||||
// f = find_char()
|
// f = find_char()
|
||||||
// g = goto (gg, G, gc, gd, etc)
|
// g = goto (gg, G, gc, gd, etc)
|
||||||
//
|
//
|
||||||
// h = move_char_left(n)
|
// h = move_char_left(n) || arrow-left = move_char_left(n)
|
||||||
// j = move_line_down(n)
|
// j = move_line_down(n) || arrow-down = move_line_down(n)
|
||||||
// k = move_line_up(n)
|
// k = move_line_up(n) || arrow_up = move_line_up(n)
|
||||||
// l = move_char_right(n)
|
// l = move_char_right(n) || arrow-right = move_char_right(n)
|
||||||
// : = command line
|
// : = command line
|
||||||
// ; = collapse selection to cursor
|
// ; = collapse selection to cursor
|
||||||
// " = use register
|
// " = use register
|
||||||
@@ -61,8 +61,8 @@ use std::collections::HashMap;
|
|||||||
// in kakoune these are alt-h alt-l / gh gl
|
// in kakoune these are alt-h alt-l / gh gl
|
||||||
// select from curs to begin end / move curs to begin end
|
// select from curs to begin end / move curs to begin end
|
||||||
// 0 = start of line
|
// 0 = start of line
|
||||||
// ^ = start of line (first non blank char)
|
// ^ = start of line(first non blank char) || Home = start of line(first non blank char)
|
||||||
// $ = end of line
|
// $ = end of line || End = end of line
|
||||||
//
|
//
|
||||||
// z = save selections
|
// z = save selections
|
||||||
// Z = restore selections
|
// Z = restore selections
|
||||||
@@ -85,6 +85,10 @@ use std::collections::HashMap;
|
|||||||
//
|
//
|
||||||
// gd = goto definition
|
// gd = goto definition
|
||||||
// gr = goto reference
|
// gr = goto reference
|
||||||
|
// [d = previous diagnostic
|
||||||
|
// d] = next diagnostic
|
||||||
|
// [D = first diagnostic
|
||||||
|
// D] = last diagnostic
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// #[cfg(feature = "term")]
|
// #[cfg(feature = "term")]
|
||||||
@@ -103,15 +107,6 @@ macro_rules! key {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! shift {
|
|
||||||
($($ch:tt)*) => {
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char($($ch)*),
|
|
||||||
modifiers: KeyModifiers::SHIFT,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! ctrl {
|
macro_rules! ctrl {
|
||||||
($($ch:tt)*) => {
|
($($ch:tt)*) => {
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
@@ -137,16 +132,40 @@ pub fn default() -> Keymaps {
|
|||||||
key!('k') => commands::move_line_up,
|
key!('k') => commands::move_line_up,
|
||||||
key!('l') => commands::move_char_right,
|
key!('l') => commands::move_char_right,
|
||||||
|
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::move_char_left,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Down,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::move_line_down,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Up,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::move_line_up,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Right,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::move_char_right,
|
||||||
|
|
||||||
key!('t') => commands::find_till_char,
|
key!('t') => commands::find_till_char,
|
||||||
key!('f') => commands::find_next_char,
|
key!('f') => commands::find_next_char,
|
||||||
shift!('T') => commands::till_prev_char,
|
key!('T') => commands::till_prev_char,
|
||||||
shift!('F') => commands::find_prev_char,
|
key!('F') => commands::find_prev_char,
|
||||||
// and matching set for select mode (extend)
|
// and matching set for select mode (extend)
|
||||||
//
|
//
|
||||||
key!('r') => commands::replace,
|
key!('r') => commands::replace,
|
||||||
|
|
||||||
key!('^') => commands::move_line_start,
|
KeyEvent {
|
||||||
key!('$') => commands::move_line_end,
|
code: KeyCode::Home,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::move_line_start,
|
||||||
|
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::End,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::move_line_end,
|
||||||
|
|
||||||
key!('w') => commands::move_next_word_start,
|
key!('w') => commands::move_next_word_start,
|
||||||
key!('b') => commands::move_prev_word_start,
|
key!('b') => commands::move_prev_word_start,
|
||||||
@@ -157,11 +176,11 @@ pub fn default() -> Keymaps {
|
|||||||
key!(':') => commands::command_mode,
|
key!(':') => commands::command_mode,
|
||||||
|
|
||||||
key!('i') => commands::insert_mode,
|
key!('i') => commands::insert_mode,
|
||||||
shift!('I') => commands::prepend_to_line,
|
key!('I') => commands::prepend_to_line,
|
||||||
key!('a') => commands::append_mode,
|
key!('a') => commands::append_mode,
|
||||||
shift!('A') => commands::append_to_line,
|
key!('A') => commands::append_to_line,
|
||||||
key!('o') => commands::open_below,
|
key!('o') => commands::open_below,
|
||||||
shift!('O') => commands::open_above,
|
key!('O') => commands::open_above,
|
||||||
// [<space> ]<space> equivalents too (add blank new line, no edit)
|
// [<space> ]<space> equivalents too (add blank new line, no edit)
|
||||||
|
|
||||||
|
|
||||||
@@ -174,12 +193,12 @@ pub fn default() -> Keymaps {
|
|||||||
|
|
||||||
key!('s') => commands::select_regex,
|
key!('s') => commands::select_regex,
|
||||||
alt!('s') => commands::split_selection_on_newline,
|
alt!('s') => commands::split_selection_on_newline,
|
||||||
shift!('S') => commands::split_selection,
|
key!('S') => commands::split_selection,
|
||||||
key!(';') => commands::collapse_selection,
|
key!(';') => commands::collapse_selection,
|
||||||
alt!(';') => commands::flip_selections,
|
alt!(';') => commands::flip_selections,
|
||||||
key!('%') => commands::select_all,
|
key!('%') => commands::select_all,
|
||||||
key!('x') => commands::select_line,
|
key!('x') => commands::select_line,
|
||||||
shift!('X') => commands::extend_line,
|
key!('X') => commands::extend_line,
|
||||||
// or select mode X?
|
// or select mode X?
|
||||||
// extend_to_whole_line, crop_to_whole_line
|
// extend_to_whole_line, crop_to_whole_line
|
||||||
|
|
||||||
@@ -194,30 +213,32 @@ pub fn default() -> Keymaps {
|
|||||||
// repeat_select
|
// repeat_select
|
||||||
|
|
||||||
// TODO: figure out what key to use
|
// TODO: figure out what key to use
|
||||||
key!('[') => commands::expand_selection,
|
// key!('[') => commands::expand_selection, ??
|
||||||
|
key!('[') => commands::left_bracket_mode,
|
||||||
|
key!(']') => commands::right_bracket_mode,
|
||||||
|
|
||||||
key!('/') => commands::search,
|
key!('/') => commands::search,
|
||||||
// ? for search_reverse
|
// ? for search_reverse
|
||||||
key!('n') => commands::search_next,
|
key!('n') => commands::search_next,
|
||||||
shift!('N') => commands::extend_search_next,
|
key!('N') => commands::extend_search_next,
|
||||||
// N for search_prev
|
// N for search_prev
|
||||||
key!('*') => commands::search_selection,
|
key!('*') => commands::search_selection,
|
||||||
|
|
||||||
key!('u') => commands::undo,
|
key!('u') => commands::undo,
|
||||||
shift!('U') => commands::redo,
|
key!('U') => commands::redo,
|
||||||
|
|
||||||
key!('y') => commands::yank,
|
key!('y') => commands::yank,
|
||||||
// yank_all
|
// yank_all
|
||||||
key!('p') => commands::paste_after,
|
key!('p') => commands::paste_after,
|
||||||
// paste_all
|
// paste_all
|
||||||
shift!('P') => commands::paste_before,
|
key!('P') => commands::paste_before,
|
||||||
|
|
||||||
key!('>') => commands::indent,
|
key!('>') => commands::indent,
|
||||||
key!('<') => commands::unindent,
|
key!('<') => commands::unindent,
|
||||||
key!('=') => commands::format_selections,
|
key!('=') => commands::format_selections,
|
||||||
shift!('J') => commands::join_selections,
|
key!('J') => commands::join_selections,
|
||||||
// TODO: conflicts hover/doc
|
// TODO: conflicts hover/doc
|
||||||
shift!('K') => commands::keep_selections,
|
key!('K') => commands::keep_selections,
|
||||||
// TODO: and another method for inverse
|
// TODO: and another method for inverse
|
||||||
|
|
||||||
// TODO: clashes with space mode
|
// TODO: clashes with space mode
|
||||||
@@ -249,14 +270,11 @@ pub fn default() -> Keymaps {
|
|||||||
ctrl!('u') => commands::half_page_up,
|
ctrl!('u') => commands::half_page_up,
|
||||||
ctrl!('d') => commands::half_page_down,
|
ctrl!('d') => commands::half_page_down,
|
||||||
|
|
||||||
KeyEvent {
|
ctrl!('w') => commands::window_mode,
|
||||||
code: KeyCode::Tab,
|
|
||||||
modifiers: KeyModifiers::NONE
|
|
||||||
} => commands::next_view,
|
|
||||||
|
|
||||||
// move under <space>c
|
// move under <space>c
|
||||||
ctrl!('c') => commands::toggle_comments,
|
ctrl!('c') => commands::toggle_comments,
|
||||||
shift!('K') => commands::hover,
|
key!('K') => commands::hover,
|
||||||
|
|
||||||
// z family for save/restore/combine from/to sels from register
|
// z family for save/restore/combine from/to sels from register
|
||||||
|
|
||||||
@@ -278,19 +296,44 @@ pub fn default() -> Keymaps {
|
|||||||
key!('k') => commands::extend_line_up,
|
key!('k') => commands::extend_line_up,
|
||||||
key!('l') => commands::extend_char_right,
|
key!('l') => commands::extend_char_right,
|
||||||
|
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::extend_char_left,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Down,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::extend_line_down,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Up,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::extend_line_up,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Right,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::extend_char_right,
|
||||||
|
|
||||||
key!('w') => commands::extend_next_word_start,
|
key!('w') => commands::extend_next_word_start,
|
||||||
key!('b') => commands::extend_prev_word_start,
|
key!('b') => commands::extend_prev_word_start,
|
||||||
key!('e') => commands::extend_next_word_end,
|
key!('e') => commands::extend_next_word_end,
|
||||||
|
|
||||||
key!('t') => commands::extend_till_char,
|
key!('t') => commands::extend_till_char,
|
||||||
key!('f') => commands::extend_next_char,
|
key!('f') => commands::extend_next_char,
|
||||||
shift!('T') => commands::extend_till_prev_char,
|
|
||||||
shift!('F') => commands::extend_prev_char,
|
|
||||||
|
|
||||||
|
key!('T') => commands::extend_till_prev_char,
|
||||||
|
key!('F') => commands::extend_prev_char,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Home,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::extend_line_start,
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::End,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
} => commands::extend_line_end,
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Esc,
|
code: KeyCode::Esc,
|
||||||
modifiers: KeyModifiers::NONE
|
modifiers: KeyModifiers::NONE
|
||||||
} => commands::exit_select_mode as Command,
|
} => commands::exit_select_mode,
|
||||||
)
|
)
|
||||||
.into_iter(),
|
.into_iter(),
|
||||||
);
|
);
|
||||||
@@ -323,6 +366,7 @@ pub fn default() -> Keymaps {
|
|||||||
} => commands::insert::insert_tab,
|
} => commands::insert::insert_tab,
|
||||||
|
|
||||||
ctrl!('x') => commands::completion,
|
ctrl!('x') => commands::completion,
|
||||||
|
ctrl!('w') => commands::insert::delete_word_backward,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -8,13 +8,11 @@ mod ui;
|
|||||||
|
|
||||||
use application::Application;
|
use application::Application;
|
||||||
|
|
||||||
use helix_core::config_dir;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context, Error, Result};
|
use anyhow::{Context, Error, Result};
|
||||||
|
|
||||||
fn setup_logging(verbosity: u64) -> Result<()> {
|
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||||
let mut base_config = fern::Dispatch::new();
|
let mut base_config = fern::Dispatch::new();
|
||||||
|
|
||||||
// Let's say we depend on something which whose "info" level messages are too
|
// Let's say we depend on something which whose "info" level messages are too
|
||||||
@@ -40,7 +38,7 @@ fn setup_logging(verbosity: u64) -> Result<()> {
|
|||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.chain(fern::log_file(config_dir().join("helix.log"))?);
|
.chain(fern::log_file(logpath)?);
|
||||||
|
|
||||||
base_config.chain(file_config).apply()?;
|
base_config.chain(file_config).apply()?;
|
||||||
|
|
||||||
@@ -96,6 +94,12 @@ fn parse_args(mut args: Args) -> Result<Args> {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
let cache_dir = helix_core::cache_dir();
|
||||||
|
if !cache_dir.exists() {
|
||||||
|
std::fs::create_dir(&cache_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let logpath = cache_dir.join("helix.log");
|
||||||
let help = format!(
|
let help = format!(
|
||||||
"\
|
"\
|
||||||
{} {}
|
{} {}
|
||||||
@@ -111,12 +115,14 @@ ARGS:
|
|||||||
FLAGS:
|
FLAGS:
|
||||||
-h, --help Prints help information
|
-h, --help Prints help information
|
||||||
-v Increases logging verbosity each use for up to 3 times
|
-v Increases logging verbosity each use for up to 3 times
|
||||||
|
(default file: {})
|
||||||
-V, --version Prints version information
|
-V, --version Prints version information
|
||||||
",
|
",
|
||||||
env!("CARGO_PKG_NAME"),
|
env!("CARGO_PKG_NAME"),
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
env!("CARGO_PKG_AUTHORS"),
|
env!("CARGO_PKG_AUTHORS"),
|
||||||
env!("CARGO_PKG_DESCRIPTION"),
|
env!("CARGO_PKG_DESCRIPTION"),
|
||||||
|
logpath.display(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut args: Args = Args {
|
let mut args: Args = Args {
|
||||||
@@ -135,29 +141,16 @@ FLAGS:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if args.display_version {
|
if args.display_version {
|
||||||
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
println!("helix {}", env!("CARGO_PKG_VERSION"));
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let conf_dir = config_dir();
|
let conf_dir = helix_core::config_dir();
|
||||||
|
|
||||||
if !conf_dir.exists() {
|
if !conf_dir.exists() {
|
||||||
std::fs::create_dir(&conf_dir);
|
std::fs::create_dir(&conf_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_logging(args.verbosity).context("failed to initialize logging")?;
|
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||||
|
|
||||||
// initialize language registry
|
|
||||||
use helix_core::syntax::{Loader, LOADER};
|
|
||||||
|
|
||||||
// load $HOME/.config/helix/languages.toml, fallback to default config
|
|
||||||
let config = std::fs::read(config_dir().join("languages.toml"));
|
|
||||||
let toml = config
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(include_bytes!("../../languages.toml"));
|
|
||||||
|
|
||||||
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
|
// TODO: use the thread local executor to spawn the application task separately from the work pool
|
||||||
let mut app = Application::new(args).context("unable to create new appliction")?;
|
let mut app = Application::new(args).context("unable to create new appliction")?;
|
||||||
|
@@ -195,7 +195,7 @@ impl EditorView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ugh,interleave highlight spans with diagnostic spans
|
// ugh,interleave highlight spans with diagnostic spans
|
||||||
let is_diagnostic = doc.diagnostics.iter().any(|diagnostic| {
|
let is_diagnostic = doc.diagnostics().iter().any(|diagnostic| {
|
||||||
diagnostic.range.start <= char_index
|
diagnostic.range.start <= char_index
|
||||||
&& diagnostic.range.end > char_index
|
&& diagnostic.range.end > char_index
|
||||||
});
|
});
|
||||||
@@ -240,7 +240,7 @@ impl EditorView {
|
|||||||
for selection in doc
|
for selection in doc
|
||||||
.selection(view.id)
|
.selection(view.id)
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|range| screen.overlaps(&range))
|
.filter(|range| range.overlaps(&screen))
|
||||||
{
|
{
|
||||||
// TODO: render also if only one of the ranges is in viewport
|
// TODO: render also if only one of the ranges is in viewport
|
||||||
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
|
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
|
||||||
@@ -261,7 +261,16 @@ impl EditorView {
|
|||||||
Rect::new(
|
Rect::new(
|
||||||
viewport.x + start.col as u16,
|
viewport.x + start.col as u16,
|
||||||
viewport.y + start.row as u16,
|
viewport.y + start.row as u16,
|
||||||
((end.col - start.col) as u16 + 1).min(viewport.width),
|
// .min is important, because set_style does a
|
||||||
|
// for i in area.left()..area.right() and
|
||||||
|
// area.right = x + width !!! which shouldn't be > then surface.area.right()
|
||||||
|
// This is checked by a debug_assert! in Buffer::index_of
|
||||||
|
((end.col - start.col) as u16 + 1).min(
|
||||||
|
surface
|
||||||
|
.area
|
||||||
|
.width
|
||||||
|
.saturating_sub(viewport.x + start.col as u16),
|
||||||
|
),
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
selection_style,
|
selection_style,
|
||||||
@@ -272,7 +281,7 @@ impl EditorView {
|
|||||||
viewport.x + start.col as u16,
|
viewport.x + start.col as u16,
|
||||||
viewport.y + start.row as u16,
|
viewport.y + start.row as u16,
|
||||||
// text.line(view.first_line).len_chars() as u16 - start.col as u16,
|
// text.line(view.first_line).len_chars() as u16 - start.col as u16,
|
||||||
viewport.width - start.col as u16,
|
viewport.width.saturating_sub(start.col as u16),
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
selection_style,
|
selection_style,
|
||||||
@@ -290,7 +299,12 @@ impl EditorView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
surface.set_style(
|
surface.set_style(
|
||||||
Rect::new(viewport.x, viewport.y + end.row as u16, end.col as u16, 1),
|
Rect::new(
|
||||||
|
viewport.x,
|
||||||
|
viewport.y + end.row as u16,
|
||||||
|
(end.col as u16).min(viewport.width),
|
||||||
|
1,
|
||||||
|
),
|
||||||
selection_style,
|
selection_style,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -306,6 +320,29 @@ impl EditorView {
|
|||||||
),
|
),
|
||||||
cursor_style,
|
cursor_style,
|
||||||
);
|
);
|
||||||
|
// TODO: set cursor position for IME
|
||||||
|
if let Some(syntax) = doc.syntax() {
|
||||||
|
use helix_core::match_brackets;
|
||||||
|
let pos = doc.selection(view.id).cursor();
|
||||||
|
let pos = match_brackets::find(syntax, doc.text(), pos);
|
||||||
|
if let Some(pos) = pos {
|
||||||
|
let pos = view.screen_coords_at_pos(doc, text, pos);
|
||||||
|
if let Some(pos) = pos {
|
||||||
|
// this only prevents panic due to painting selection too far
|
||||||
|
// TODO: prevent painting when scroll past x or in gutter
|
||||||
|
// TODO: use a more correct width check
|
||||||
|
if (pos.col as u16) < viewport.width {
|
||||||
|
let style = Style::default().add_modifier(Modifier::REVERSED);
|
||||||
|
surface
|
||||||
|
.get_mut(
|
||||||
|
viewport.x + pos.col as u16,
|
||||||
|
viewport.y + pos.row as u16,
|
||||||
|
)
|
||||||
|
.set_style(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,9 +355,9 @@ impl EditorView {
|
|||||||
let info: Style = theme.get("info");
|
let info: Style = theme.get("info");
|
||||||
let hint: Style = theme.get("hint");
|
let hint: Style = theme.get("hint");
|
||||||
|
|
||||||
for (i, line) in (view.first_line..last_line).enumerate() {
|
for (i, line) in (view.first_line..=last_line).enumerate() {
|
||||||
use helix_core::diagnostic::Severity;
|
use helix_core::diagnostic::Severity;
|
||||||
if let Some(diagnostic) = doc.diagnostics.iter().find(|d| d.line == line) {
|
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
|
||||||
surface.set_stringn(
|
surface.set_stringn(
|
||||||
viewport.x - OFFSET,
|
viewport.x - OFFSET,
|
||||||
viewport.y + i as u16,
|
viewport.y + i as u16,
|
||||||
@@ -364,7 +401,7 @@ impl EditorView {
|
|||||||
let cursor = doc.selection(view.id).cursor();
|
let cursor = doc.selection(view.id).cursor();
|
||||||
let line = doc.text().char_to_line(cursor);
|
let line = doc.text().char_to_line(cursor);
|
||||||
|
|
||||||
let diagnostics = doc.diagnostics.iter().filter(|diagnostic| {
|
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
|
||||||
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
|
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -446,7 +483,7 @@ impl EditorView {
|
|||||||
surface.set_stringn(
|
surface.set_stringn(
|
||||||
viewport.x + viewport.width.saturating_sub(15),
|
viewport.x + viewport.width.saturating_sub(15),
|
||||||
viewport.y,
|
viewport.y,
|
||||||
format!("{}", doc.diagnostics.len()),
|
format!("{}", doc.diagnostics().len()),
|
||||||
4,
|
4,
|
||||||
text_color,
|
text_color,
|
||||||
);
|
);
|
||||||
@@ -529,7 +566,8 @@ impl Component for EditorView {
|
|||||||
cx.editor.resize(Rect::new(0, 0, width, height - 1));
|
cx.editor.resize(Rect::new(0, 0, width, height - 1));
|
||||||
EventResult::Consumed(None)
|
EventResult::Consumed(None)
|
||||||
}
|
}
|
||||||
Event::Key(key) => {
|
Event::Key(mut key) => {
|
||||||
|
canonicalize_key(&mut key);
|
||||||
// clear status
|
// clear status
|
||||||
cx.editor.status_msg = None;
|
cx.editor.status_msg = None;
|
||||||
|
|
||||||
@@ -676,3 +714,13 @@ impl Component for EditorView {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonicalize_key(key: &mut KeyEvent) {
|
||||||
|
if let KeyEvent {
|
||||||
|
code: KeyCode::Char(_),
|
||||||
|
modifiers: _,
|
||||||
|
} = key
|
||||||
|
{
|
||||||
|
key.modifiers.remove(KeyModifiers::SHIFT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -100,8 +100,11 @@ impl<T> Picker<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_down(&mut self) {
|
pub fn move_down(&mut self) {
|
||||||
// TODO: len - 1
|
if self.matches.is_empty() {
|
||||||
if self.cursor < self.options.len() {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cursor < self.matches.len() - 1 {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,13 +246,14 @@ impl<T: 'static> Component for Picker<T> {
|
|||||||
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
|
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
|
||||||
|
|
||||||
let rows = inner.height - 2; // -1 for search bar
|
let rows = inner.height - 2; // -1 for search bar
|
||||||
|
let offset = self.cursor / (rows as usize) * (rows as usize);
|
||||||
|
|
||||||
let files = self.matches.iter().map(|(index, _score)| {
|
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
|
||||||
(index, self.options.get(*index).unwrap()) // get_unchecked
|
(index, self.options.get(*index).unwrap()) // get_unchecked
|
||||||
});
|
});
|
||||||
|
|
||||||
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
|
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
|
||||||
if i == self.cursor {
|
if i == (self.cursor - offset) {
|
||||||
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
|
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +262,11 @@ impl<T: 'static> Component for Picker<T> {
|
|||||||
inner.y + 2 + i as u16,
|
inner.y + 2 + i as u16,
|
||||||
(self.format_fn)(option),
|
(self.format_fn)(option),
|
||||||
inner.width as usize - 1,
|
inner.width as usize - 1,
|
||||||
if i == self.cursor { selected } else { style },
|
if i == (self.cursor - offset) {
|
||||||
|
selected
|
||||||
|
} else {
|
||||||
|
style
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,11 @@ pub enum PromptEvent {
|
|||||||
Abort,
|
Abort,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum CompletionDirection {
|
||||||
|
Forward,
|
||||||
|
Backward,
|
||||||
|
}
|
||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
prompt: String,
|
prompt: String,
|
||||||
@@ -80,11 +85,18 @@ impl Prompt {
|
|||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn change_completion_selection(&mut self) {
|
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
|
||||||
if self.completion.is_empty() {
|
if self.completion.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let index = self.selection.map_or(0, |i| i + 1) % self.completion.len();
|
|
||||||
|
let index = match direction {
|
||||||
|
CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1),
|
||||||
|
CompletionDirection::Backward => {
|
||||||
|
self.selection.unwrap_or(0) + self.completion.len() - 1
|
||||||
|
}
|
||||||
|
} % self.completion.len();
|
||||||
|
|
||||||
self.selection = Some(index);
|
self.selection = Some(index);
|
||||||
|
|
||||||
let (range, item) = &self.completion[index];
|
let (range, item) = &self.completion[index];
|
||||||
@@ -92,8 +104,8 @@ impl Prompt {
|
|||||||
self.line.replace_range(range.clone(), item);
|
self.line.replace_range(range.clone(), item);
|
||||||
|
|
||||||
self.move_end();
|
self.move_end();
|
||||||
// TODO: recalculate completion when completion item is accepted, (Enter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exit_selection(&mut self) {
|
pub fn exit_selection(&mut self) {
|
||||||
self.selection = None;
|
self.selection = None;
|
||||||
}
|
}
|
||||||
@@ -114,7 +126,7 @@ impl Prompt {
|
|||||||
let selected_color = theme.get("ui.menu.selected");
|
let selected_color = theme.get("ui.menu.selected");
|
||||||
// completion
|
// completion
|
||||||
|
|
||||||
let max_col = area.width / BASE_WIDTH;
|
let max_col = std::cmp::max(1, area.width / BASE_WIDTH);
|
||||||
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
|
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
|
||||||
let completion_area = Rect::new(
|
let completion_area = Rect::new(
|
||||||
area.x,
|
area.x,
|
||||||
@@ -253,12 +265,21 @@ impl Component for Prompt {
|
|||||||
code: KeyCode::Enter,
|
code: KeyCode::Enter,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
|
if self.line.ends_with('/') {
|
||||||
return close_fn;
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
|
self.exit_selection();
|
||||||
|
} else {
|
||||||
|
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
|
||||||
|
return close_fn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Tab, ..
|
code: KeyCode::Tab, ..
|
||||||
} => self.change_completion_selection(),
|
} => self.change_completion_selection(CompletionDirection::Forward),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::BackTab,
|
||||||
|
..
|
||||||
|
} => self.change_completion_selection(CompletionDirection::Backward),
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char('q'),
|
code: KeyCode::Char('q'),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helix-tui"
|
name = "helix-tui"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
description = """
|
description = """
|
||||||
A library to build rich terminal user interfaces or dashboards
|
A library to build rich terminal user interfaces or dashboards
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
use helix_tui::{
|
use helix_tui::{
|
||||||
backend::{Backend, TestBackend},
|
backend::{Backend, TestBackend},
|
||||||
layout::Rect,
|
|
||||||
widgets::Paragraph,
|
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn terminal_buffer_size_should_be_limited() {
|
fn terminal_buffer_size_should_be_limited() {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helix-view"
|
name = "helix-view"
|
||||||
version = "0.1.0"
|
version = "0.0.10"
|
||||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -28,3 +28,4 @@ slotmap = "1"
|
|||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
log = "~0.4"
|
||||||
|
@@ -48,7 +48,7 @@ pub struct Document {
|
|||||||
last_saved_revision: usize,
|
last_saved_revision: usize,
|
||||||
version: i32, // should be usize?
|
version: i32, // should be usize?
|
||||||
|
|
||||||
pub diagnostics: Vec<Diagnostic>,
|
diagnostics: Vec<Diagnostic>,
|
||||||
language_server: Option<Arc<helix_lsp::Client>>,
|
language_server: Option<Arc<helix_lsp::Client>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +104,14 @@ pub fn normalize_path(path: &Path) -> PathBuf {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the canonical, absolute form of a path with all intermediate components normalized.
|
||||||
|
//
|
||||||
|
// This function is used instead of `std::fs::canonicalize` because we don't want to verify
|
||||||
|
// here if the path exists, just normalize it's components.
|
||||||
|
pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
|
||||||
|
std::env::current_dir().map(|current_dir| normalize_path(¤t_dir.join(path)))
|
||||||
|
}
|
||||||
|
|
||||||
use helix_lsp::lsp;
|
use helix_lsp::lsp;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@@ -131,27 +139,20 @@ impl Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: passing scopes here is awkward
|
|
||||||
// TODO: async fn?
|
// TODO: async fn?
|
||||||
pub fn load(path: PathBuf, scopes: &[String]) -> Result<Self, Error> {
|
pub fn load(path: PathBuf) -> Result<Self, Error> {
|
||||||
use std::{env, fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader};
|
||||||
let _current_dir = env::current_dir()?;
|
|
||||||
|
|
||||||
let file = File::open(path.clone()).context(format!("unable to open {:?}", path))?;
|
let doc = if !path.exists() {
|
||||||
let doc = Rope::from_reader(BufReader::new(file))?;
|
Rope::from("\n")
|
||||||
|
} else {
|
||||||
// TODO: create if not found
|
let file = File::open(&path).context(format!("unable to open {:?}", path))?;
|
||||||
|
Rope::from_reader(BufReader::new(file))?
|
||||||
|
};
|
||||||
|
|
||||||
let mut doc = Self::new(doc);
|
let mut doc = Self::new(doc);
|
||||||
|
// set the path and try detecting the language
|
||||||
let language_config = LOADER
|
doc.set_path(&path)?;
|
||||||
.get()
|
|
||||||
.unwrap()
|
|
||||||
.language_config_for_file_name(path.as_path());
|
|
||||||
doc.set_language(language_config, scopes);
|
|
||||||
|
|
||||||
// canonicalize path to absolute value
|
|
||||||
doc.path = Some(std::fs::canonicalize(path)?);
|
|
||||||
|
|
||||||
Ok(doc)
|
Ok(doc)
|
||||||
}
|
}
|
||||||
@@ -200,6 +201,14 @@ impl Document {
|
|||||||
|
|
||||||
async move {
|
async move {
|
||||||
use tokio::{fs::File, io::AsyncWriteExt};
|
use tokio::{fs::File, io::AsyncWriteExt};
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
// TODO: display a prompt asking the user if the directories should be created
|
||||||
|
if !parent.exists() {
|
||||||
|
return Err(Error::msg(
|
||||||
|
"can't save file, parent directory does not exist",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut file = File::create(path).await?;
|
let mut file = File::create(path).await?;
|
||||||
|
|
||||||
// write all the rope chunks to file
|
// write all the rope chunks to file
|
||||||
@@ -218,17 +227,25 @@ impl Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
|
fn detect_language(&mut self) {
|
||||||
// canonicalize path to absolute value
|
if let Some(path) = self.path() {
|
||||||
let current_dir = std::env::current_dir()?;
|
let loader = LOADER.get().unwrap();
|
||||||
let path = normalize_path(¤t_dir.join(path));
|
let language_config = loader.language_config_for_file_name(path);
|
||||||
|
let scopes = loader.scopes();
|
||||||
if let Some(parent) = path.parent() {
|
self.set_language(language_config, scopes);
|
||||||
// TODO: return error as necessary
|
|
||||||
if parent.exists() {
|
|
||||||
self.path = Some(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
|
||||||
|
let path = canonicalize_path(path)?;
|
||||||
|
|
||||||
|
// if parent doesn't exist we still want to open the document
|
||||||
|
// and error out when document is saved
|
||||||
|
self.path = Some(path);
|
||||||
|
|
||||||
|
// try detecting the language based on filepath
|
||||||
|
self.detect_language();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,8 +268,10 @@ impl Document {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_language2(&mut self, scope: &str, scopes: &[String]) {
|
pub fn set_language2(&mut self, scope: &str) {
|
||||||
let language_config = LOADER.get().unwrap().language_config_for_scope(scope);
|
let loader = LOADER.get().unwrap();
|
||||||
|
let language_config = loader.language_config_for_scope(scope);
|
||||||
|
let scopes = loader.scopes();
|
||||||
|
|
||||||
self.set_language(language_config, scopes);
|
self.set_language(language_config, scopes);
|
||||||
}
|
}
|
||||||
@@ -342,29 +361,35 @@ impl Document {
|
|||||||
|
|
||||||
pub fn undo(&mut self, view_id: ViewId) -> bool {
|
pub fn undo(&mut self, view_id: ViewId) -> bool {
|
||||||
let mut history = self.history.take();
|
let mut history = self.history.take();
|
||||||
if let Some(transaction) = history.undo() {
|
let success = if let Some(transaction) = history.undo() {
|
||||||
let success = self._apply(&transaction, view_id);
|
self._apply(&transaction, view_id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.history.set(history);
|
||||||
|
|
||||||
|
if success {
|
||||||
// reset changeset to fix len
|
// reset changeset to fix len
|
||||||
self.changes = ChangeSet::new(self.text());
|
self.changes = ChangeSet::new(self.text());
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
self.history.set(history);
|
|
||||||
false
|
success
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redo(&mut self, view_id: ViewId) -> bool {
|
pub fn redo(&mut self, view_id: ViewId) -> bool {
|
||||||
let mut history = self.history.take();
|
let mut history = self.history.take();
|
||||||
if let Some(transaction) = history.redo() {
|
let success = if let Some(transaction) = history.redo() {
|
||||||
let success = self._apply(&transaction, view_id);
|
self._apply(&transaction, view_id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.history.set(history);
|
||||||
|
|
||||||
|
if success {
|
||||||
// reset changeset to fix len
|
// reset changeset to fix len
|
||||||
self.changes = ChangeSet::new(self.text());
|
self.changes = ChangeSet::new(self.text());
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
self.history.set(history);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +519,14 @@ impl Document {
|
|||||||
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
|
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
|
||||||
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
|
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn diagnostics(&self) -> &[Diagnostic] {
|
||||||
|
&self.diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
|
||||||
|
self.diagnostics = diagnostics;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@@ -36,6 +36,18 @@ impl Editor {
|
|||||||
.unwrap_or(include_bytes!("../../theme.toml"));
|
.unwrap_or(include_bytes!("../../theme.toml"));
|
||||||
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
|
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
|
||||||
|
|
||||||
|
// initialize language registry
|
||||||
|
use helix_core::syntax::{Loader, LOADER};
|
||||||
|
|
||||||
|
// load $HOME/.config/helix/languages.toml, fallback to default config
|
||||||
|
let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
|
||||||
|
let toml = config
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(include_bytes!("../../languages.toml"));
|
||||||
|
|
||||||
|
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
|
||||||
|
LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
|
||||||
|
|
||||||
let language_servers = helix_lsp::Registry::new();
|
let language_servers = helix_lsp::Registry::new();
|
||||||
|
|
||||||
// HAXX: offset the render area height by 1 to account for prompt/commandline
|
// HAXX: offset the render area height by 1 to account for prompt/commandline
|
||||||
@@ -125,7 +137,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
|
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
|
||||||
let path = std::fs::canonicalize(path)?;
|
let path = crate::document::canonicalize_path(&path)?;
|
||||||
|
|
||||||
let id = self
|
let id = self
|
||||||
.documents()
|
.documents()
|
||||||
@@ -135,7 +147,7 @@ impl Editor {
|
|||||||
let id = if let Some(id) = id {
|
let id = if let Some(id) = id {
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
let mut doc = Document::load(path, self.theme.scopes())?;
|
let mut doc = Document::load(path)?;
|
||||||
|
|
||||||
// try to find a language server based on the language name
|
// try to find a language server based on the language name
|
||||||
let language_server = doc
|
let language_server = doc
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
#[cfg(feature = "term")]
|
#[cfg(feature = "term")]
|
||||||
pub use tui::style::{Color, Style};
|
pub use tui::style::{Color, Modifier, Style};
|
||||||
|
|
||||||
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
|
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||||
// pub struct Color {
|
// pub struct Color {
|
||||||
@@ -115,6 +116,7 @@ impl<'de> Deserialize<'de> for Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_style(style: &mut Style, value: Value) {
|
fn parse_style(style: &mut Style, value: Value) {
|
||||||
|
//TODO: alert user of parsing failures
|
||||||
if let Value::Table(entries) = value {
|
if let Value::Table(entries) = value {
|
||||||
for (name, value) in entries {
|
for (name, value) in entries {
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
@@ -128,6 +130,13 @@ fn parse_style(style: &mut Style, value: Value) {
|
|||||||
*style = style.bg(color);
|
*style = style.bg(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"modifiers" => {
|
||||||
|
if let Value::Array(arr) = value {
|
||||||
|
for modifier in arr.iter().filter_map(parse_modifier) {
|
||||||
|
*style = style.add_modifier(modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,9 +166,34 @@ fn parse_color(value: Value) -> Option<Color> {
|
|||||||
if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
|
if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
|
||||||
Some(Color::Rgb(red, green, blue))
|
Some(Color::Rgb(red, green, blue))
|
||||||
} else {
|
} else {
|
||||||
|
warn!("malformed hexcode in theme: {}", s);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
warn!("unrecognized value in theme: {}", value);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_modifier(value: &Value) -> Option<Modifier> {
|
||||||
|
if let Value::String(s) = value {
|
||||||
|
match s.as_str() {
|
||||||
|
"bold" => Some(Modifier::BOLD),
|
||||||
|
"dim" => Some(Modifier::DIM),
|
||||||
|
"italic" => Some(Modifier::ITALIC),
|
||||||
|
"underlined" => Some(Modifier::UNDERLINED),
|
||||||
|
"slow_blink" => Some(Modifier::SLOW_BLINK),
|
||||||
|
"rapid_blink" => Some(Modifier::RAPID_BLINK),
|
||||||
|
"reversed" => Some(Modifier::REVERSED),
|
||||||
|
"hidden" => Some(Modifier::HIDDEN),
|
||||||
|
"crossed_out" => Some(Modifier::CROSSED_OUT),
|
||||||
|
_ => {
|
||||||
|
warn!("unrecognized modifier in theme: {}", s);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("unrecognized modifier in theme: {}", value);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,3 +211,39 @@ impl Theme {
|
|||||||
&self.scopes
|
&self.scopes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_style_string() {
|
||||||
|
let fg = Value::String("#ffffff".to_string());
|
||||||
|
|
||||||
|
let mut style = Style::default();
|
||||||
|
parse_style(&mut style, fg);
|
||||||
|
|
||||||
|
assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_style_table() {
|
||||||
|
let table = toml::toml! {
|
||||||
|
"keyword" = {
|
||||||
|
fg = "#ffffff",
|
||||||
|
bg = "#000000",
|
||||||
|
modifiers = ["bold"],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut style = Style::default();
|
||||||
|
if let Value::Table(entries) = table {
|
||||||
|
for (_name, value) in entries {
|
||||||
|
parse_style(&mut style, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
style,
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Rgb(255, 255, 255))
|
||||||
|
.bg(Color::Rgb(0, 0, 0))
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -106,7 +106,7 @@ impl View {
|
|||||||
/// Calculates the last visible line on screen
|
/// Calculates the last visible line on screen
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn last_line(&self, doc: &Document) -> usize {
|
pub fn last_line(&self, doc: &Document) -> usize {
|
||||||
let height = self.area.height.saturating_sub(1); // - 1 for statusline
|
let height = self.area.height.saturating_sub(2); // - 2 for statusline
|
||||||
std::cmp::min(
|
std::cmp::min(
|
||||||
self.first_line + height as usize,
|
self.first_line + height as usize,
|
||||||
doc.text().len_lines() - 1,
|
doc.text().len_lines() - 1,
|
||||||
|
@@ -42,6 +42,7 @@ injection-regex = "c"
|
|||||||
file-types = ["c"] # TODO: ["h"]
|
file-types = ["c"] # TODO: ["h"]
|
||||||
roots = []
|
roots = []
|
||||||
|
|
||||||
|
language-server = { command = "clangd" }
|
||||||
indent = { tab-width = 2, unit = " " }
|
indent = { tab-width = 2, unit = " " }
|
||||||
|
|
||||||
[[language]]
|
[[language]]
|
||||||
@@ -51,6 +52,7 @@ injection-regex = "cpp"
|
|||||||
file-types = ["cc", "cpp", "hpp", "h"]
|
file-types = ["cc", "cpp", "hpp", "h"]
|
||||||
roots = []
|
roots = []
|
||||||
|
|
||||||
|
language-server = { command = "clangd" }
|
||||||
indent = { tab-width = 2, unit = " " }
|
indent = { tab-width = 2, unit = " " }
|
||||||
|
|
||||||
[[language]]
|
[[language]]
|
||||||
@@ -142,3 +144,12 @@ file-types = ["php"]
|
|||||||
roots = []
|
roots = []
|
||||||
|
|
||||||
indent = { tab-width = 2, unit = " " }
|
indent = { tab-width = 2, unit = " " }
|
||||||
|
|
||||||
|
# [[language]]
|
||||||
|
# name = "haskell"
|
||||||
|
# scope = "source.haskell"
|
||||||
|
# injection-regex = "haskell"
|
||||||
|
# file-types = ["hs"]
|
||||||
|
# roots = []
|
||||||
|
#
|
||||||
|
# indent = { tab-width = 2, unit = " " }
|
||||||
|
43
runtime/queries/haskell/highlights.scm
Normal file
43
runtime/queries/haskell/highlights.scm
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
(variable) @variable
|
||||||
|
(operator) @operator
|
||||||
|
(exp_name (constructor) @constructor)
|
||||||
|
(constructor_operator) @operator
|
||||||
|
(module) @module_name
|
||||||
|
(type) @type
|
||||||
|
(type) @class
|
||||||
|
(constructor) @constructor
|
||||||
|
(pragma) @pragma
|
||||||
|
(comment) @comment
|
||||||
|
(signature name: (variable) @fun_type_name)
|
||||||
|
(function name: (variable) @fun_name)
|
||||||
|
(constraint class: (class_name (type)) @class)
|
||||||
|
(class (class_head class: (class_name (type)) @class))
|
||||||
|
(instance (instance_head class: (class_name (type)) @class))
|
||||||
|
(integer) @literal
|
||||||
|
(exp_literal (float)) @literal
|
||||||
|
(char) @literal
|
||||||
|
(con_unit) @literal
|
||||||
|
(con_list) @literal
|
||||||
|
(tycon_arrow) @operator
|
||||||
|
(where) @keyword
|
||||||
|
"module" @keyword
|
||||||
|
"let" @keyword
|
||||||
|
"in" @keyword
|
||||||
|
"class" @keyword
|
||||||
|
"instance" @keyword
|
||||||
|
"data" @keyword
|
||||||
|
"newtype" @keyword
|
||||||
|
"family" @keyword
|
||||||
|
"type" @keyword
|
||||||
|
"import" @keyword
|
||||||
|
"qualified" @keyword
|
||||||
|
"as" @keyword
|
||||||
|
"deriving" @keyword
|
||||||
|
"via" @keyword
|
||||||
|
"stock" @keyword
|
||||||
|
"anyclass" @keyword
|
||||||
|
"do" @keyword
|
||||||
|
"mdo" @keyword
|
||||||
|
"rec" @keyword
|
||||||
|
"(" @paren
|
||||||
|
")" @paren
|
4
runtime/queries/haskell/locals.scm
Normal file
4
runtime/queries/haskell/locals.scm
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
(signature name: (variable)) @local.definition
|
||||||
|
(function name: (variable)) @local.definition
|
||||||
|
(pat_name (variable)) @local.definition
|
||||||
|
(exp_name (variable)) @local.reference
|
@@ -34,6 +34,9 @@
|
|||||||
; Namespaces
|
; Namespaces
|
||||||
|
|
||||||
(crate) @namespace
|
(crate) @namespace
|
||||||
|
(extern_crate_declaration
|
||||||
|
(crate)
|
||||||
|
name: (identifier) @namespace)
|
||||||
(scoped_use_list
|
(scoped_use_list
|
||||||
path: (identifier) @namespace)
|
path: (identifier) @namespace)
|
||||||
(scoped_use_list
|
(scoped_use_list
|
||||||
@@ -62,11 +65,9 @@
|
|||||||
function: (field_expression
|
function: (field_expression
|
||||||
field: (field_identifier) @function.method))
|
field: (field_identifier) @function.method))
|
||||||
|
|
||||||
; (macro_invocation
|
|
||||||
; macro: (identifier) @function.macro
|
|
||||||
; "!" @function.macro)
|
|
||||||
(macro_invocation
|
(macro_invocation
|
||||||
macro: (identifier) @function.macro)
|
macro: (identifier) @function.macro
|
||||||
|
"!" @function.macro)
|
||||||
(macro_invocation
|
(macro_invocation
|
||||||
macro: (scoped_identifier
|
macro: (scoped_identifier
|
||||||
(identifier) @function.macro .))
|
(identifier) @function.macro .))
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
|
|
||||||
(lifetime (identifier) @label)
|
(lifetime (identifier) @label)
|
||||||
|
|
||||||
|
"async" @keyword
|
||||||
"break" @keyword
|
"break" @keyword
|
||||||
"const" @keyword
|
"const" @keyword
|
||||||
"continue" @keyword
|
"continue" @keyword
|
||||||
@@ -144,7 +146,7 @@
|
|||||||
"use" @keyword
|
"use" @keyword
|
||||||
"where" @keyword
|
"where" @keyword
|
||||||
"while" @keyword
|
"while" @keyword
|
||||||
(mutable_specifier) @keyword
|
(mutable_specifier) @keyword.mut
|
||||||
(use_list (self) @keyword)
|
(use_list (self) @keyword)
|
||||||
(scoped_use_list (self) @keyword)
|
(scoped_use_list (self) @keyword)
|
||||||
(scoped_identifier (self) @keyword)
|
(scoped_identifier (self) @keyword)
|
||||||
|
Reference in New Issue
Block a user