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
on:
pull_request:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
check:
@@ -49,7 +49,7 @@ jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
@@ -60,7 +60,7 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: ${{ matrix.rust }}
override: true
- name: Cache cargo registry
@@ -86,6 +86,11 @@ jobs:
with:
command: test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
lints:
name: Lints
runs-on: ubuntu-latest

View File

@@ -37,6 +37,10 @@ jobs:
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu

47
Cargo.lock generated
View File

@@ -259,13 +259,14 @@ dependencies = [
[[package]]
name = "helix-core"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"etcetera",
"helix-syntax",
"once_cell",
"regex",
"ropey",
"rust-embed",
"serde",
"smallvec",
"tendril",
@@ -277,7 +278,7 @@ dependencies = [
[[package]]
name = "helix-lsp"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"futures-executor",
@@ -299,7 +300,7 @@ dependencies = [
[[package]]
name = "helix-syntax"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"cc",
"serde",
@@ -309,7 +310,7 @@ dependencies = [
[[package]]
name = "helix-term"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"chrono",
@@ -335,7 +336,7 @@ dependencies = [
[[package]]
name = "helix-tui"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"bitflags",
"cassowary",
@@ -347,13 +348,14 @@ dependencies = [
[[package]]
name = "helix-view"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"crossterm",
"helix-core",
"helix-lsp",
"helix-tui",
"log",
"once_cell",
"serde",
"slotmap",
@@ -692,6 +694,39 @@ dependencies = [
"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]]
name = "ryu"
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
[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
- 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
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
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
> 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
There are two packages available from AUR:
- `helix-bin`: contains prebuilt binary from GitHub releases
- `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
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
a good overview of the internals.
## Usage
### Keyboard shortcuts / Keymap
All shortcuts/keymaps can be found in the documentation on the website:
- https://docs.helix-editor.com/keymap.html
# Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-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
## 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
runtime inside the same folder as the executable, but that can be overriden via
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 |
|-----|-----------|
| h | move left |
| j | move down |
| k | move up |
| l | move right |
| h, Left | move left |
| j, Down | move down |
| k, Up | move up |
| l, Right | move right |
| w | move next word start |
| b | move previous word start |
| e | move next word end |
@@ -17,20 +17,20 @@
| f | find next char |
| T | find 'till previous char |
| F | find previous char |
| ^ | move to the start of the line |
| $ | move to the end of the line |
| Home | move to the start of the line |
| End | move to the end of the line |
| m | Jump to matching bracket |
| PageUp | Move page up |
| PageDown | Move page down |
| ctrl-u | Move half page up |
| ctrl-d | Move half page down |
| Tab | Switch to next view |
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
| ctrl-o | Jump backward on the jumplist |
| v | Enter select (extend) mode |
| g | Enter goto mode |
| : | Enter command mode |
| z | Enter view mode |
| ctrl-w | Enter window mode (maybe will be remove for spc w w later) |
| space | Enter space mode |
| 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 |
| * | 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
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 |
| 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 |
| t | Go to type definition |
| y | Go to type definition |
| r | Go to references |
| i | Go to implementation |
@@ -127,6 +144,17 @@ Jumps to various locations.
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
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 |
| b | Open buffer picker |
| v | Open a new vertical split into the current file |
| w | Save changes to file |
| c | Close the current split |
| w | Enter window mode |
| 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]
name = "helix-core"
version = "0.1.0"
version = "0.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
embed_runtime = ["rust-embed"]
[dependencies]
helix-syntax = { path = "../helix-syntax" }
@@ -24,3 +28,4 @@ serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
etcetera = "0.3"
rust-embed = { version = "5.9.0", optional = true }

View File

@@ -251,22 +251,25 @@ where
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
};
use once_cell::sync::OnceCell;
let loader = Loader::new(Configuration {
language: vec![LanguageConfiguration {
scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()],
language_id: Lang::Rust,
highlight_config: OnceCell::new(),
//
roots: vec![],
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
}],
});
let loader = Loader::new(
Configuration {
language: vec![LanguageConfiguration {
scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()],
language_id: Lang::Rust,
highlight_config: OnceCell::new(),
//
roots: vec![],
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
}],
},
Vec::new(),
);
// set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));

View File

@@ -16,6 +16,7 @@ pub mod selection;
mod state;
pub mod syntax;
mod transaction;
pub mod words;
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
// find first non-whitespace char
@@ -44,6 +45,7 @@ pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -
None
}
#[cfg(not(embed_runtime))]
pub fn runtime_dir() -> std::path::PathBuf {
// runtime env var || dir where binary is located
std::env::var("HELIX_RUNTIME")
@@ -58,13 +60,22 @@ pub fn runtime_dir() -> std::path::PathBuf {
pub fn config_dir() -> std::path::PathBuf {
// 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 mut path = strategy.config_dir();
path.push("helix");
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 tendril::StrTendril as Tendril;

View File

@@ -1,6 +1,6 @@
use crate::{Range, Rope, Selection, Syntax};
// const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']')];
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
// limit matching pairs to only ( ) { } [ ] < >
#[must_use]
@@ -20,15 +20,27 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
None => return None,
};
let start_byte = node.start_byte();
let end_byte = node.end_byte() - 1; // it's end exclusive
if start_byte == byte_pos {
return Some(doc.byte_to_char(end_byte));
if node.is_error() {
return None;
}
if end_byte == byte_pos {
return Some(doc.byte_to_char(start_byte));
let start_byte = node.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

View File

@@ -45,7 +45,10 @@ pub fn move_vertically(
let new_line = match dir {
Direction::Backward => row.saturating_sub(count),
Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 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
@@ -171,22 +174,24 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
// used for by-word movement
fn is_word(ch: char) -> bool {
pub(crate) fn is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
fn is_horiz_blank(ch: char) -> bool {
pub(crate) fn is_horiz_blank(ch: char) -> bool {
matches!(ch, ' ' | '\t')
}
#[derive(Debug, Eq, PartialEq)]
enum Category {
pub(crate) enum Category {
Whitespace,
Eol,
Word,
Punctuation,
Unknown,
}
fn categorize(ch: char) -> Category {
pub(crate) fn categorize(ch: char) -> Category {
if ch == '\n' {
Category::Eol
} else if ch.is_ascii_whitespace() {
@@ -196,7 +201,7 @@ fn categorize(ch: char) -> Category {
} else if ch.is_ascii_punctuation() {
Category::Punctuation
} else {
unreachable!()
Category::Unknown
}
}

View File

@@ -41,7 +41,7 @@ pub fn find_nth_prev(
inclusive: bool,
) -> Option<usize> {
// 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 {
loop {
@@ -56,7 +56,7 @@ pub fn find_nth_prev(
}
if !inclusive {
pos -= 1;
pos += 1;
}
Some(pos)

View File

@@ -383,7 +383,7 @@ pub fn split_on_matches(
// TODO: retain range direction
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());
}

View File

@@ -73,16 +73,46 @@ pub struct IndentQuery {
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 {
static INHERITS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
let root = crate::runtime_dir();
// 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();
let query = load_runtime_file(language, filename).unwrap_or_default();
// TODO: the collect() is not ideal
let inherits = INHERITS_REGEX
@@ -146,11 +176,8 @@ impl LanguageConfiguration {
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let root = crate::runtime_dir();
let path = root.join("queries").join(language).join("indents.toml");
let toml = std::fs::read(&path).ok()?;
toml::from_slice(&toml).ok()
let toml = load_runtime_file(&language, "indents.toml").ok()?;
toml::from_slice(&toml.as_bytes()).ok()
})
.as_ref()
}
@@ -166,13 +193,15 @@ pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
scopes: Vec<String>,
}
impl Loader {
pub fn new(config: Configuration) -> Self {
pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(),
scopes,
};
for config in config.language {
@@ -192,6 +221,10 @@ impl Loader {
loader
}
pub fn scopes(&self) -> &[String] {
&self.scopes
}
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this 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;
}
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() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
@@ -754,4 +755,21 @@ mod test {
use Operation::*;
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]
name = "helix-lsp"
version = "0.1.0"
version = "0.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"

View File

@@ -1,5 +1,4 @@
mod client;
mod select_all;
mod transport;
pub use jsonrpc_core as jsonrpc;
@@ -171,7 +170,7 @@ pub use jsonrpc::Call;
type LanguageId = String;
use crate::select_all::SelectAll;
use futures_util::stream::select_all::SelectAll;
pub struct Registry {
inner: HashMap<LanguageId, Option<Arc<Client>>>,
@@ -198,7 +197,7 @@ impl Registry {
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &self.incoming;
let s_incoming = &mut self.incoming;
let language_server = inner
.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]
name = "helix-syntax"
version = "0.1.0"
version = "0.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"

View File

@@ -1,16 +1,8 @@
use std::fs;
use std::path::PathBuf;
use std::{env, fs};
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> {
let mut dirs = Vec::new();
for entry in fs::read_dir("languages").unwrap().flatten() {
@@ -58,25 +50,28 @@ fn build_c(files: Vec<String>, language: &str) {
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.opt_level(get_opt_level())
.debug(get_debug())
.warnings(false)
.flag_if_supported("-std=c99");
.warnings(false);
}
build.compile(&format!("tree-sitter-{}-c", language));
}
fn build_cpp(files: Vec<String>, language: &str) {
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 {
build
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.opt_level(get_opt_level())
.debug(get_debug())
.warnings(false)
.cpp(true);
.cpp(true)
.flag_if_supported(flag);
}
build.compile(&format!("tree-sitter-{}-cpp", language));
}
@@ -109,6 +104,7 @@ fn build_dir(dir: &str, language: &str) {
fn main() {
let ignore = vec![
"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(),
];
let dirs = collect_tree_sitter_dirs(&ignore);

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-term"
version = "0.1.0"
version = "0.0.10"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
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
[features]
embed_runtime = ["helix-core/embed_runtime"]
[[bin]]
name = "hx"
path = "src/main.rs"

View File

@@ -45,16 +45,28 @@ impl Application {
let size = compositor.size();
let mut editor = Editor::new(size);
compositor.push(Box::new(ui::EditorView::new()));
if !args.files.is_empty() {
for file in args.files {
editor.open(file, Action::VerticalSplit)?;
let first = &args.files[0]; // we know it's not empty
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 {
editor.new_file(Action::VerticalSplit);
}
compositor.push(Box::new(ui::EditorView::new()));
let mut app = Self {
compositor,
editor,
@@ -205,7 +217,7 @@ impl Application {
})
.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
self.render();
}

View File

@@ -3,8 +3,8 @@ use helix_core::{
movement::{self, Direction},
object, pos_at_coords,
regex::{self, Regex},
register, search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection,
SmallVec, Tendril, Transaction,
register, search, selection, words, Change, ChangeSet, Position, Range, Rope, RopeSlice,
Selection, SmallVec, Tendril, Transaction,
};
use helix_view::{
@@ -315,7 +315,7 @@ fn _find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
where
// TODO: make an options struct for and abstract this Fn into a searcher type
// 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.
// Would be nice to carry over.
@@ -332,7 +332,7 @@ where
let text = doc.text().slice(..);
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 {
Range::new(range.anchor, pos)
} else {
@@ -475,8 +475,8 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
// clamp into viewport
let line = cursor
.row
.min(view.first_line + scrolloff)
.max(last_line.saturating_sub(scrolloff));
.max(view.first_line + scrolloff)
.min(last_line.saturating_sub(scrolloff));
let text = doc.text().slice(..);
let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
@@ -573,6 +573,37 @@ pub fn extend_line_down(cx: &mut Context) {
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) {
let (view, doc) = cx.current();
@@ -638,7 +669,7 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
return;
}
let head = end - 1;
let head = end;
let selection = if extend {
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 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));
}
@@ -759,7 +792,9 @@ fn _delete_selection(doc: &mut Document, view_id: ViewId) {
// then delete
let transaction =
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);
}
@@ -769,6 +804,9 @@ pub fn delete_selection(cx: &mut Context) {
_delete_selection(doc, 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) {
@@ -1182,7 +1220,7 @@ fn open(cx: &mut Context, open: Open) {
let text = text.repeat(count);
// calculate new selection range
let pos = index + text.len();
let pos = index + text.chars().count();
ranges.push(Range::new(pos, pos));
(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
// to 1g
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));
return;
}
@@ -1263,10 +1302,35 @@ pub fn goto_mode(cx: &mut Context) {
match ch {
'g' => move_file_start(cx),
'e' => move_file_end(cx),
'h' => move_line_start(cx),
'l' => move_line_end(cx),
'd' => goto_definition(cx),
't' => goto_type_definition(cx),
'y' => goto_type_definition(cx),
'r' => goto_reference(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) {
let (view, doc) = cx.current();
@@ -1667,7 +1811,7 @@ pub mod insert {
text.push('\n');
text.push_str(&indent);
let head = pos + offs + text.len();
let head = pos + offs + text.chars().count();
// TODO: range replace or extend
// 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);
}
offs += text.len();
offs += text.chars().count();
(pos, pos, Some(text.into()))
});
@@ -1718,7 +1862,6 @@ pub mod insert {
pub fn delete_char_forward(cx: &mut Context) {
let count = cx.count;
let doc = cx.doc();
let (view, doc) = cx.current();
let text = doc.text().slice(..);
let transaction =
@@ -1731,6 +1874,21 @@ pub mod insert {
});
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
@@ -2176,11 +2334,6 @@ pub fn hover(cx: &mut Context) {
);
}
// view movements
pub fn next_view(cx: &mut Context) {
cx.editor.focus_next()
}
// comments
pub fn toggle_comments(cx: &mut Context) {
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;
let (view, doc) = cx.current();
let id = doc.id();
let selection = doc.selection(view.id).clone();
let first_line = view.first_line;
cx.editor.switch(id, Action::VerticalSplit);
cx.editor.switch(id, action);
// match the selection in the previous view
let (view, doc) = cx.current();
@@ -2261,6 +2436,20 @@ pub fn vsplit(cx: &mut Context) {
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) {
cx.on_next_key(move |cx, event| {
if let KeyEvent {
@@ -2272,18 +2461,7 @@ pub fn space_mode(cx: &mut Context) {
match ch {
'f' => file_picker(cx),
'b' => buffer_picker(cx),
'v' => vsplit(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);
}
'w' => window_mode(cx),
// ' ' => toggle_alternate_buffer(cx),
// TODO: temporary since space mode took it's old key
' ' => keep_primary_selection(cx),
@@ -2324,7 +2502,7 @@ pub fn view_mode(cx: &mut Context) {
let pos = coords_at_pos(doc.text().slice(..), pos);
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' => (),
'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()
// g = goto (gg, G, gc, gd, etc)
//
// h = move_char_left(n)
// j = move_line_down(n)
// k = move_line_up(n)
// l = move_char_right(n)
// h = move_char_left(n) || arrow-left = move_char_left(n)
// j = move_line_down(n) || arrow-down = move_line_down(n)
// k = move_line_up(n) || arrow_up = move_line_up(n)
// l = move_char_right(n) || arrow-right = move_char_right(n)
// : = command line
// ; = collapse selection to cursor
// " = use register
@@ -61,8 +61,8 @@ use std::collections::HashMap;
// in kakoune these are alt-h alt-l / gh gl
// select from curs to begin end / move curs to begin end
// 0 = start of line
// ^ = start of line (first non blank char)
// $ = end of line
// ^ = start of line(first non blank char) || Home = start of line(first non blank char)
// $ = end of line || End = end of line
//
// z = save selections
// Z = restore selections
@@ -85,6 +85,10 @@ use std::collections::HashMap;
//
// gd = goto definition
// gr = goto reference
// [d = previous diagnostic
// d] = next diagnostic
// [D = first diagnostic
// D] = last diagnostic
// }
// #[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 {
($($ch:tt)*) => {
KeyEvent {
@@ -137,16 +132,40 @@ pub fn default() -> Keymaps {
key!('k') => commands::move_line_up,
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!('f') => commands::find_next_char,
shift!('T') => commands::till_prev_char,
shift!('F') => commands::find_prev_char,
key!('T') => commands::till_prev_char,
key!('F') => commands::find_prev_char,
// and matching set for select mode (extend)
//
key!('r') => commands::replace,
key!('^') => commands::move_line_start,
key!('$') => commands::move_line_end,
KeyEvent {
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!('b') => commands::move_prev_word_start,
@@ -157,11 +176,11 @@ pub fn default() -> Keymaps {
key!(':') => commands::command_mode,
key!('i') => commands::insert_mode,
shift!('I') => commands::prepend_to_line,
key!('I') => commands::prepend_to_line,
key!('a') => commands::append_mode,
shift!('A') => commands::append_to_line,
key!('A') => commands::append_to_line,
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)
@@ -174,12 +193,12 @@ pub fn default() -> Keymaps {
key!('s') => commands::select_regex,
alt!('s') => commands::split_selection_on_newline,
shift!('S') => commands::split_selection,
key!('S') => commands::split_selection,
key!(';') => commands::collapse_selection,
alt!(';') => commands::flip_selections,
key!('%') => commands::select_all,
key!('x') => commands::select_line,
shift!('X') => commands::extend_line,
key!('X') => commands::extend_line,
// or select mode X?
// extend_to_whole_line, crop_to_whole_line
@@ -194,30 +213,32 @@ pub fn default() -> Keymaps {
// repeat_select
// 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,
// ? for search_reverse
key!('n') => commands::search_next,
shift!('N') => commands::extend_search_next,
key!('N') => commands::extend_search_next,
// N for search_prev
key!('*') => commands::search_selection,
key!('u') => commands::undo,
shift!('U') => commands::redo,
key!('U') => commands::redo,
key!('y') => commands::yank,
// yank_all
key!('p') => commands::paste_after,
// paste_all
shift!('P') => commands::paste_before,
key!('P') => commands::paste_before,
key!('>') => commands::indent,
key!('<') => commands::unindent,
key!('=') => commands::format_selections,
shift!('J') => commands::join_selections,
key!('J') => commands::join_selections,
// TODO: conflicts hover/doc
shift!('K') => commands::keep_selections,
key!('K') => commands::keep_selections,
// TODO: and another method for inverse
// TODO: clashes with space mode
@@ -249,14 +270,11 @@ pub fn default() -> Keymaps {
ctrl!('u') => commands::half_page_up,
ctrl!('d') => commands::half_page_down,
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE
} => commands::next_view,
ctrl!('w') => commands::window_mode,
// move under <space>c
ctrl!('c') => commands::toggle_comments,
shift!('K') => commands::hover,
key!('K') => commands::hover,
// 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!('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!('b') => commands::extend_prev_word_start,
key!('e') => commands::extend_next_word_end,
key!('t') => commands::extend_till_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 {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::exit_select_mode as Command,
} => commands::exit_select_mode,
)
.into_iter(),
);
@@ -323,6 +366,7 @@ pub fn default() -> Keymaps {
} => commands::insert::insert_tab,
ctrl!('x') => commands::completion,
ctrl!('w') => commands::insert::delete_word_backward,
),
)
}

View File

@@ -8,13 +8,11 @@ mod ui;
use application::Application;
use helix_core::config_dir;
use std::path::PathBuf;
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's say we depend on something which whose "info" level messages are too
@@ -40,7 +38,7 @@ fn setup_logging(verbosity: u64) -> Result<()> {
message
))
})
.chain(fern::log_file(config_dir().join("helix.log"))?);
.chain(fern::log_file(logpath)?);
base_config.chain(file_config).apply()?;
@@ -96,6 +94,12 @@ fn parse_args(mut args: Args) -> Result<Args> {
#[tokio::main]
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!(
"\
{} {}
@@ -111,12 +115,14 @@ ARGS:
FLAGS:
-h, --help Prints help information
-v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
);
let mut args: Args = Args {
@@ -135,29 +141,16 @@ FLAGS:
}
if args.display_version {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
println!("helix {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let conf_dir = config_dir();
let conf_dir = helix_core::config_dir();
if !conf_dir.exists() {
std::fs::create_dir(&conf_dir);
}
setup_logging(args.verbosity).context("failed to initialize logging")?;
// initialize language registry
use helix_core::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));
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
// 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")?;

View File

@@ -195,7 +195,7 @@ impl EditorView {
}
// 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.end > char_index
});
@@ -240,7 +240,7 @@ impl EditorView {
for selection in doc
.selection(view.id)
.iter()
.filter(|range| screen.overlaps(&range))
.filter(|range| range.overlaps(&screen))
{
// TODO: render also if only one of the ranges is in viewport
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
@@ -261,7 +261,16 @@ impl EditorView {
Rect::new(
viewport.x + start.col 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,
),
selection_style,
@@ -272,7 +281,7 @@ impl EditorView {
viewport.x + start.col as u16,
viewport.y + start.row 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,
),
selection_style,
@@ -290,7 +299,12 @@ impl EditorView {
);
}
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,
);
}
@@ -306,6 +320,29 @@ impl EditorView {
),
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 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;
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(
viewport.x - OFFSET,
viewport.y + i as u16,
@@ -364,7 +401,7 @@ impl EditorView {
let cursor = doc.selection(view.id).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
});
@@ -446,7 +483,7 @@ impl EditorView {
surface.set_stringn(
viewport.x + viewport.width.saturating_sub(15),
viewport.y,
format!("{}", doc.diagnostics.len()),
format!("{}", doc.diagnostics().len()),
4,
text_color,
);
@@ -529,7 +566,8 @@ impl Component for EditorView {
cx.editor.resize(Rect::new(0, 0, width, height - 1));
EventResult::Consumed(None)
}
Event::Key(key) => {
Event::Key(mut key) => {
canonicalize_key(&mut key);
// clear status
cx.editor.status_msg = None;
@@ -676,3 +714,13 @@ impl Component for EditorView {
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) {
// TODO: len - 1
if self.cursor < self.options.len() {
if self.matches.is_empty() {
return;
}
if self.cursor < self.matches.len() - 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 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
});
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);
}
@@ -258,7 +262,11 @@ impl<T: 'static> Component for Picker<T> {
inner.y + 2 + i as u16,
(self.format_fn)(option),
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,
}
pub enum CompletionDirection {
Forward,
Backward,
}
impl Prompt {
pub fn new(
prompt: String,
@@ -80,11 +85,18 @@ impl Prompt {
self.exit_selection();
}
pub fn change_completion_selection(&mut self) {
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
if self.completion.is_empty() {
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);
let (range, item) = &self.completion[index];
@@ -92,8 +104,8 @@ impl Prompt {
self.line.replace_range(range.clone(), item);
self.move_end();
// TODO: recalculate completion when completion item is accepted, (Enter)
}
pub fn exit_selection(&mut self) {
self.selection = None;
}
@@ -114,7 +126,7 @@ impl Prompt {
let selected_color = theme.get("ui.menu.selected");
// 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 completion_area = Rect::new(
area.x,
@@ -253,12 +265,21 @@ impl Component for Prompt {
code: KeyCode::Enter,
..
} => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
return close_fn;
if self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
} else {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
return close_fn;
}
}
KeyEvent {
code: KeyCode::Tab, ..
} => self.change_completion_selection(),
} => self.change_completion_selection(CompletionDirection::Forward),
KeyEvent {
code: KeyCode::BackTab,
..
} => self.change_completion_selection(CompletionDirection::Backward),
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-tui"
version = "0.1.0"
version = "0.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """
A library to build rich terminal user interfaces or dashboards

View File

@@ -1,10 +1,7 @@
use helix_tui::{
backend::{Backend, TestBackend},
layout::Rect,
widgets::Paragraph,
Terminal,
};
use std::error::Error;
#[test]
fn terminal_buffer_size_should_be_limited() {

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-view"
version = "0.1.0"
version = "0.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
@@ -28,3 +28,4 @@ slotmap = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = "~0.4"

View File

@@ -48,7 +48,7 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
pub diagnostics: Vec<Diagnostic>,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
}
@@ -104,6 +104,14 @@ pub fn normalize_path(path: &Path) -> PathBuf {
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 url::Url;
@@ -131,27 +139,20 @@ impl Document {
}
}
// TODO: passing scopes here is awkward
// TODO: async fn?
pub fn load(path: PathBuf, scopes: &[String]) -> Result<Self, Error> {
use std::{env, fs::File, io::BufReader};
let _current_dir = env::current_dir()?;
pub fn load(path: PathBuf) -> Result<Self, Error> {
use std::{fs::File, io::BufReader};
let file = File::open(path.clone()).context(format!("unable to open {:?}", path))?;
let doc = Rope::from_reader(BufReader::new(file))?;
// TODO: create if not found
let doc = if !path.exists() {
Rope::from("\n")
} else {
let file = File::open(&path).context(format!("unable to open {:?}", path))?;
Rope::from_reader(BufReader::new(file))?
};
let mut doc = Self::new(doc);
let language_config = LOADER
.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)?);
// set the path and try detecting the language
doc.set_path(&path)?;
Ok(doc)
}
@@ -200,6 +201,14 @@ impl Document {
async move {
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?;
// 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> {
// canonicalize path to absolute value
let current_dir = std::env::current_dir()?;
let path = normalize_path(&current_dir.join(path));
if let Some(parent) = path.parent() {
// TODO: return error as necessary
if parent.exists() {
self.path = Some(path);
}
fn detect_language(&mut self) {
if let Some(path) = self.path() {
let loader = LOADER.get().unwrap();
let language_config = loader.language_config_for_file_name(path);
let scopes = loader.scopes();
self.set_language(language_config, scopes);
}
}
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(())
}
@@ -251,8 +268,10 @@ impl Document {
};
}
pub fn set_language2(&mut self, scope: &str, scopes: &[String]) {
let language_config = LOADER.get().unwrap().language_config_for_scope(scope);
pub fn set_language2(&mut self, scope: &str) {
let loader = LOADER.get().unwrap();
let language_config = loader.language_config_for_scope(scope);
let scopes = loader.scopes();
self.set_language(language_config, scopes);
}
@@ -342,29 +361,35 @@ impl Document {
pub fn undo(&mut self, view_id: ViewId) -> bool {
let mut history = self.history.take();
if let Some(transaction) = history.undo() {
let success = self._apply(&transaction, view_id);
let success = if let Some(transaction) = history.undo() {
self._apply(&transaction, view_id)
} else {
false
};
self.history.set(history);
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
return success;
}
self.history.set(history);
false
success
}
pub fn redo(&mut self, view_id: ViewId) -> bool {
let mut history = self.history.take();
if let Some(transaction) = history.redo() {
let success = self._apply(&transaction, view_id);
let success = if let Some(transaction) = history.redo() {
self._apply(&transaction, view_id)
} else {
false
};
self.history.set(history);
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
return success;
}
self.history.set(history);
false
}
@@ -494,6 +519,14 @@ impl Document {
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
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)]

View File

@@ -36,6 +36,18 @@ impl Editor {
.unwrap_or(include_bytes!("../../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();
// 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> {
let path = std::fs::canonicalize(path)?;
let path = crate::document::canonicalize_path(&path)?;
let id = self
.documents()
@@ -135,7 +147,7 @@ impl Editor {
let id = if let Some(id) = id {
id
} 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
let language_server = doc

View File

@@ -1,10 +1,11 @@
use std::collections::HashMap;
use log::warn;
use serde::{Deserialize, Deserializer};
use toml::Value;
#[cfg(feature = "term")]
pub use tui::style::{Color, Style};
pub use tui::style::{Color, Modifier, Style};
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
// pub struct Color {
@@ -115,6 +116,7 @@ impl<'de> Deserialize<'de> for Theme {
}
fn parse_style(style: &mut Style, value: Value) {
//TODO: alert user of parsing failures
if let Value::Table(entries) = value {
for (name, value) in entries {
match name.as_str() {
@@ -128,6 +130,13 @@ fn parse_style(style: &mut Style, value: Value) {
*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) {
Some(Color::Rgb(red, green, blue))
} else {
warn!("malformed hexcode in theme: {}", s);
None
}
} 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
}
}
@@ -177,3 +211,39 @@ impl Theme {
&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
#[inline]
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(
self.first_line + height as usize,
doc.text().len_lines() - 1,

View File

@@ -42,6 +42,7 @@ injection-regex = "c"
file-types = ["c"] # TODO: ["h"]
roots = []
language-server = { command = "clangd" }
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -51,6 +52,7 @@ injection-regex = "cpp"
file-types = ["cc", "cpp", "hpp", "h"]
roots = []
language-server = { command = "clangd" }
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -142,3 +144,12 @@ file-types = ["php"]
roots = []
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
(crate) @namespace
(extern_crate_declaration
(crate)
name: (identifier) @namespace)
(scoped_use_list
path: (identifier) @namespace)
(scoped_use_list
@@ -62,11 +65,9 @@
function: (field_expression
field: (field_identifier) @function.method))
; (macro_invocation
; macro: (identifier) @function.macro
; "!" @function.macro)
(macro_invocation
macro: (identifier) @function.macro)
macro: (identifier) @function.macro
"!" @function.macro)
(macro_invocation
macro: (scoped_identifier
(identifier) @function.macro .))
@@ -111,6 +112,7 @@
(lifetime (identifier) @label)
"async" @keyword
"break" @keyword
"const" @keyword
"continue" @keyword
@@ -144,7 +146,7 @@
"use" @keyword
"where" @keyword
"while" @keyword
(mutable_specifier) @keyword
(mutable_specifier) @keyword.mut
(use_list (self) @keyword)
(scoped_use_list (self) @keyword)
(scoped_identifier (self) @keyword)