Compare commits

...

83 Commits

Author SHA1 Message Date
Blaž Hrastnik
59f94d13b8 Disable haskell grammar until build issues are resolved 2021-06-07 10:17:25 +09:00
Blaž Hrastnik
b3eeac7bbf Disable aarch64-macos, it fails to build on macos-latest 2021-06-07 09:50:15 +09:00
Blaž Hrastnik
f48a60b8e2 Release 0.0.10 2021-06-07 09:42:15 +09:00
Blaž Hrastnik
4f561e93b8 View mode: Use saturating_sub when calculating first_col 2021-06-07 09:29:21 +09:00
Blaž Hrastnik
01b1bd15a1 commands: use chars().count() over .len() on strings 2021-06-07 09:26:49 +09:00
Blaž Hrastnik
ff8a031cb2 Add diagnostics keys to keymap.md 2021-06-07 09:24:23 +09:00
Blaž Hrastnik
d9b2f6feac Only test on stable rust
Shorter CI times, and it should be good enough.
2021-06-07 09:20:36 +09:00
Blaž Hrastnik
582f1ee9d8 Add aarch64-macos (M1) to the release build matrix 2021-06-07 09:19:51 +09:00
ahkrr
e2d780f993 fix: 2 panics while setting style + off by 1
The panics would occur because set_style 
would draw outside of the the surface. 
Both occured using `find_prev` or `till_prev`
In my case the first panic! would appear
in a terminal with around 80 columns 
in helix/README.md going to the end of the file
with `geglf(`
the second with `geglfX`
The off by one fix ensures that `find_nth_prev` 
starts at the first character to the left
2021-06-07 09:15:08 +09:00
Ethan Bodzioney
843c2cdebd Install instructions and version number corrections (#148)
* Add MacOS install instructions

* Change version name argument

When using the -V command to get the version you are given 'helix-term x.x.x', I changed this to just helix as it makes more sense.

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number
2021-06-07 09:14:06 +09:00
Benoît CORTIER
8a29086c1a Fix panic when moving over unicode punctuation
`is_ascii_punctuation` will only work for ASCII punctuations, and when
we have unicode punctuation (or other) we jump into the `unreachable`.
This patch fallback into categorizing everything in this branch as
`Unknown`.

Fixes https://github.com/helix-editor/helix/issues/123

https://github.com/helix-editor/helix/pull/135: add better support for
unicode categories.
2021-06-07 09:12:01 +09:00
Wojciech Kępka
16b1cfa3be Add diagnostics keybindings 2021-06-07 09:11:52 +09:00
Ivan Tham
2066e866c7 Add spc w w for window mode 2021-06-07 09:08:08 +09:00
Kevin Sjöberg
3494bb8ef0 Refactor index assignment
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 21:48:19 +09:00
Kevin Sjöberg
a4ff8cdd8a Allow moving backwards in completions 2021-06-06 21:48:19 +09:00
Kevin Sjöberg
145bc1970a Trigger directory completion upon pressing Enter 2021-06-06 21:48:19 +09:00
Ingrid
54f3548d54 theme: Enable style modifiers in theme.toml, add Ingrid's theme (#113)
* theme: Enable style modifiers in theme.toml

* docs: theme documentation

* fixup: parse modifiers with filter_map

* theme: tests for parse_style

* theme: Log invalid cases in theme.toml parse

* docs: theme documentation fixup

* docs: Blaz's theming comments

* docs: Theme doc fixes from pickfire

Co-authored-by: Ivan Tham <pickfire@riseup.net>

* theme: More context in logs, TODO for alerting users

* contrib: Ingrid's theme

* docs: Theme subsection fixes

Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 21:45:59 +09:00
Ivan Tham
3280510d5b Fix unused import 2021-06-06 21:30:18 +09:00
Ivan Tham
df80f3c966 Add test for prev word 2021-06-06 21:30:18 +09:00
Ivan Tham
40744ce835 Add ctrl-w in insert mode
It seemed to panic when I pressed too many times, but that is from
lsp side.
2021-06-06 21:30:18 +09:00
Kevin Sjöberg
aa8a8baeeb Calculate offset when moving picker cursor 2021-06-06 19:18:09 +09:00
Wojciech Kępka
bcb1afeb4c Add a comment to canonicalize_path 2021-06-06 17:28:09 +08:00
Wojciech Kępka
de946d2357 Add a TODO 2021-06-06 17:28:09 +08:00
Wojciech Kępka
14f511da93 Create document if it doesn't exist on save 2021-06-06 17:28:09 +08:00
Blaž Hrastnik
392631b21d Update build.yml 2021-06-06 18:22:18 +09:00
Ivan Tham
ce99ecc7a2 Add more coverage for CI
Runs every day as cron. Add matrix for test, includes windows and macos.
2021-06-06 18:10:02 +09:00
Kevin Sjöberg
2ac496f919 Do not move past number of matches 2021-06-06 18:04:45 +09:00
Brian Dawn
5463a436a8 Return an error if we request an embedded file that does not exist.
This makes the load_runtime_file function behave like the non-embedded
one.
2021-06-06 10:49:17 +09:00
Brian Dawn
e09b0f4eff Add a smoke test around loading runtime files.
This test makes sure we can read some amount of data from the runtime folder.
2021-06-06 10:49:17 +09:00
Brian Dawn
f3db12e240 Simplify the load_runtime_file code.
Reduce the number of feature switches for the embed_runtime feature.
2021-06-06 10:49:17 +09:00
Brian Dawn
676719b361 Simplify creating pathbufs.
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 10:49:17 +09:00
Brian Dawn
ae105812d6 Apply suggestions from code review
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 10:49:17 +09:00
Brian Dawn
255598a2cb Make rust-embed optionally included based on the embed_runtime feature. 2021-06-06 10:49:17 +09:00
Brian Dawn
62d181de78 Provide a feature flag to be able to embed the runtime folder.
These changes provide a new feature flag "embed_runtime" that when
enabled and built in release mode will embed the runtime folder into the
resulting binary.
2021-06-06 10:49:17 +09:00
Ivan Tham
8c2fa12ffc Add window mode
Fix #93
2021-06-06 10:12:35 +09:00
Jan Hrastnik
212f6bc372 changed flag in build_cpp '/std:c++14' to '/std:c++17' due to tree_sitter_haskell not compiling on msvc without it 2021-06-06 09:27:58 +09:00
ahkrr
c5c3ec07f4 fix: panicked at 'attempt to subtract with overflow'
helix-term/src/ui/editor.rs:275:29
This would happen when the window-size was to small to display the entire width and one would start jumping forwards with f<some_char> and the beginning of the highlighted area would end up outside of the window
2021-06-06 00:01:16 +09:00
ahkrr
444cd0b068 fix: make find_prev_char and till_prev_char work
Bevore this PR `commands::find_prev_char` and `commands::till_prev_char` were triggerable through keys 
but `seach::find_nth_next()` was hardcoded in `_find_char`. 
The passed `fn` was nerver used. With this PR the passed `fn` is used.
The change in search.rs resolves an off by one error in the behivor of `find_nth_prev`
2021-06-06 00:01:16 +09:00
Blaž Hrastnik
f6a900fee1 syntax: Use a different C++ flag for MSVC 2021-06-06 00:00:18 +09:00
Ivan Tham
6254720f53 Add unreachable context
Better error for #123
2021-06-05 20:18:27 +08:00
Blaž Hrastnik
407b37c327 Better link to Matrix 2021-06-05 13:04:13 +09:00
notoria
2bb71a829e Don't panic on empty file/buffer (#108) 2021-06-05 13:00:43 +09:00
Kirawi
c17dcb8633 Fixing Multiple Panics (#121)
* init

* wip

* wip
2021-06-05 12:49:19 +09:00
Blaž Hrastnik
5a344a3ae5 Address clippy lint 2021-06-05 09:28:13 +09:00
Antoni Stevenet
a1f4b8f92b Add home-end keymaps, (as kakoune/vim do) (#83)
* add home-end keymaps

* implement extend methods for extend_line_start, extend_line_end

* add home-end mappings to keymaps.md

* add ^-$ extend mappings for extend mode

* pass cargo linter
2021-06-05 09:25:46 +09:00
Blaž Hrastnik
72eaaaac99 syntax: Build C++ grammars as c++14
The haskell grammar requires at last c++14 to build.

Fixes #117
2021-06-05 09:21:33 +09:00
Blaž Hrastnik
8f78c0c612 syntax: Disable explicit debug/opt_level passing
cc-rs will already do the right thing and figure out the flags.

Fixes #34
2021-06-05 09:20:33 +09:00
Corey Powell
01dd7b570a Restored haskell syntax
It seems to work
2021-06-05 01:17:44 +08:00
notoria
f3a243c6cb Rust: Highlight crate namespace, categorize mut 2021-06-04 23:16:33 +09:00
notoria
adcfcf9044 Replace ^/$ with gh/gl 2021-06-04 17:26:16 +09:00
Blaž Hrastnik
4f0e3aa948 Implement gt/gm/gb, remap goto tYpe to gy 2021-06-04 15:47:29 +09:00
Blaž Hrastnik
f2e554d761 matchbrackets: Needs to render with the viewport offset 2021-06-04 15:11:55 +09:00
Blaž Hrastnik
bd4552cd2b scroll: Fix the clamping 2021-06-04 11:36:28 +09:00
Blaž Hrastnik
06d8d3f55f Try to detect language when document file path is set
Fixes #91
2021-06-04 11:03:40 +09:00
Blaž Hrastnik
8afd4e1bc2 Exit select mode on delete_selection 2021-06-04 11:03:40 +09:00
wojciechkepka
43b92b24d2 Show file picker when directory passed as first arg 2021-06-04 11:02:06 +09:00
notoria
b2b2d430ae Rust: Add keyword async, match the entire macro 2021-06-04 10:57:17 +09:00
notoria
8af5a9a5cf Remove swapfile 2021-06-04 10:30:14 +09:00
notoria
f76f44c8af Convert byte index to char index for find 2021-06-04 10:00:22 +09:00
Egor Karavaev
d55419604c Remove select_all implementation 2021-06-04 09:25:30 +09:00
Ivan Tham
29b9eed33c Fix panic paint mysterious matching pair
When the matching pair is out of bounds it still paints it causing an
out of bound panic. A dirty fix since it still have some issue, at least
it does not panic now.
2021-06-04 09:25:03 +09:00
Kevin Sjöberg
fdb5bfafae Limit goto count
Giving a goto count greater than the number of lines in the buffer
would cause Helix to panic.
2021-06-04 01:35:52 +09:00
Ivan Tham
e6132f0acd Fix undo redo
I missed the fast return.

Fix #89
2021-06-04 01:27:09 +09:00
Antoni Stevent
3071339cbc update keymap.md to include arrow keys for movement 2021-06-03 23:24:24 +09:00
Antoni Stevent
27aee705e0 use correct _extend methods, also remove unnecessary casts 2021-06-03 23:24:24 +09:00
Antoni Stevent
f0fe558f38 Add up/right/left/down arrow keymaps, similar to kakoune 2021-06-03 23:24:24 +09:00
Jakub Bartodziej
09a7db637e Avoid theoretical underflow. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
31ed4db153 Clean up leftover log. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
3c5dfb0633 Improve on the fix for deleting from the end of the buffer. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
6cbc0aea92 Disable deleting from an empty buffer which can cause a crash. 2021-06-03 23:23:23 +09:00
Jan Hrastnik
c1c3750d38 key is now modified in place at start of handle_event 2021-06-03 23:16:04 +09:00
Jan Hrastnik
daad8ebe12 key_canonicalization now only matches chars 2021-06-03 23:16:04 +09:00
Jan Hrastnik
68abc67ec6 put the key canonicalization in a seperate function. only chars now get stripped of Shift modifier 2021-06-03 23:16:04 +09:00
Jan Hrastnik
712f25c2b9 removed shift matching 2021-06-03 23:16:04 +09:00
Blaž Hrastnik
abe8a83d8e Merge pull request #92 from bfredl/clangd
LSP: add clangd as server for c/c++
2021-06-03 22:23:20 +09:00
Blaž Hrastnik
a05fb95769 Merge pull request #80 from notoria/highlight
Highlight matching brackets
2021-06-03 22:14:37 +09:00
Blaž Hrastnik
74e4ac8d49 Merge pull request #77 from notoria/match_brackets
Fix match_brackets::find
2021-06-03 22:13:48 +09:00
Björn Linse
0e6f007028 LSP: add clangd as server for c/c++ 2021-06-03 15:07:50 +02:00
notoria
c3a98b6a3e Highlight matching brackets 2021-06-03 11:40:46 +02:00
notoria
4fe654cf9a Fix match_brackets::find 2021-06-03 10:35:17 +02:00
Blaž Hrastnik
661dbdca57 Fix cursor not showing on (0, 0) 2021-06-03 13:34:00 +09:00
Blaž Hrastnik
5773bd6a40 Merge pull request #64 from pickfire/log
Default log file to cache
2021-06-03 12:58:31 +09:00
Ivan Tham
d664d1dec0 Default log file to cache 2021-06-03 10:15:17 +08:00
44 changed files with 1178 additions and 421 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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"

View File

@@ -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).

View File

@@ -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.

View File

@@ -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"
```

View File

@@ -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
View 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)!

View 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"

View File

@@ -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 }

View File

@@ -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"));

View File

@@ -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;

View File

@@ -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

View File

@@ -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
} }
} }

View File

@@ -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)

View File

@@ -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());
} }

View File

@@ -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());
}

View File

@@ -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
View 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=");
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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"

View File

@@ -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);

View File

@@ -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"

View File

@@ -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();
} }

View File

@@ -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),
_ => (),
}
}
})
}

View File

@@ -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,
), ),
) )
} }

View File

@@ -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")?;

View File

@@ -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)
}
}

View File

@@ -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
},
); );
} }
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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(&current_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(&current_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)]

View File

@@ -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

View File

@@ -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)
);
}

View File

@@ -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,

View File

@@ -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 = " " }

View 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

View 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

View File

@@ -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)