Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection
Rebasing was making me manually fix conflicts on every commit, so merging instead.
This commit is contained in:
commit
e686c3e462
4
.github/ISSUE_TEMPLATE/blank_issue.md
vendored
Normal file
4
.github/ISSUE_TEMPLATE/blank_issue.md
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
name: Blank Issue
|
||||||
|
about: Create a blank issue.
|
||||||
|
---
|
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a new feature or improvement
|
||||||
|
title: ''
|
||||||
|
labels: C-enchancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Your feature may already be reported!
|
||||||
|
Please search on the issue tracker before creating one. -->
|
||||||
|
|
||||||
|
#### Describe your feature request
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -68,7 +68,7 @@ jobs:
|
|||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --locked
|
args: --release --locked
|
||||||
|
|
||||||
- name: Build release binary
|
- name: Build release binary
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
|
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -17,6 +17,12 @@ version = "1.0.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
|
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -134,6 +140,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -254,6 +266,7 @@ dependencies = [
|
|||||||
name = "helix-core"
|
name = "helix-core"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"helix-syntax",
|
"helix-syntax",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -354,6 +367,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1057,6 +1071,16 @@ version = "0.10.2+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -3,6 +3,7 @@ # Summary
|
|||||||
- [Installation](./install.md)
|
- [Installation](./install.md)
|
||||||
- [Usage](./usage.md)
|
- [Usage](./usage.md)
|
||||||
- [Configuration](./configuration.md)
|
- [Configuration](./configuration.md)
|
||||||
|
- [Themes](./themes.md)
|
||||||
- [Keymap](./keymap.md)
|
- [Keymap](./keymap.md)
|
||||||
- [Key Remapping](./remapping.md)
|
- [Key Remapping](./remapping.md)
|
||||||
- [Hooks](./hooks.md)
|
- [Hooks](./hooks.md)
|
||||||
|
@ -1,97 +1,10 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
|
||||||
|
|
||||||
## LSP
|
## LSP
|
||||||
|
|
||||||
To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
|
To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
|
||||||
```toml
|
```toml
|
||||||
lsp-progress = false
|
lsp-progress = false
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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` | For lifetimes |
|
|
||||||
| `module` | |
|
|
||||||
| `ui.background` | |
|
|
||||||
| `ui.linenr` | |
|
|
||||||
| `ui.linenr.selected` | For lines with cursors |
|
|
||||||
| `ui.statusline` | |
|
|
||||||
| `ui.popup` | |
|
|
||||||
| `ui.window` | |
|
|
||||||
| `ui.help` | |
|
|
||||||
| `ui.text` | |
|
|
||||||
| `ui.text.focus` | |
|
|
||||||
| `ui.menu.selected` | |
|
|
||||||
| `ui.selection` | For selections in the editing area |
|
|
||||||
| `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.
|
|
||||||
|
|
||||||
|
@ -69,9 +69,8 @@ ### Selection manipulation
|
|||||||
| `;` | Collapse selection onto a single cursor |
|
| `;` | Collapse selection onto a single cursor |
|
||||||
| `Alt-;` | Flip selection cursor and anchor |
|
| `Alt-;` | Flip selection cursor and anchor |
|
||||||
| `%` | Select entire file |
|
| `%` | Select entire file |
|
||||||
| `x` | Select current line |
|
| `x` | Select current line, if already selected, extend to next line |
|
||||||
| `X` | Extend to next line |
|
| `` | Expand selection to parent syntax node TODO: pick a key |
|
||||||
| `[` | Expand selection to parent syntax node TODO: pick a key |
|
|
||||||
| `J` | join lines inside selection |
|
| `J` | join lines inside selection |
|
||||||
| `K` | keep selections matching the regex TODO: overlapped by hover help |
|
| `K` | keep selections matching the regex TODO: overlapped by hover help |
|
||||||
| `Space` | keep only the primary selection TODO: overlapped by space mode |
|
| `Space` | keep only the primary selection TODO: overlapped by space mode |
|
||||||
@ -155,10 +154,10 @@ ## Window mode
|
|||||||
|
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
| ----- | ------------- |
|
| ----- | ------------- |
|
||||||
| `w`, `ctrl-w` | Switch to next window |
|
| `w`, `Ctrl-w` | Switch to next window |
|
||||||
| `v`, `ctrl-v` | Vertical right split |
|
| `v`, `Ctrl-v` | Vertical right split |
|
||||||
| `h`, `ctrl-h` | Horizontal bottom split |
|
| `h`, `Ctrl-h` | Horizontal bottom split |
|
||||||
| `q`, `ctrl-q` | Close current window |
|
| `q`, `Ctrl-q` | Close current window |
|
||||||
|
|
||||||
## Space mode
|
## Space mode
|
||||||
|
|
||||||
@ -171,6 +170,11 @@ ## Space mode
|
|||||||
| `s` | Open symbol picker (current document) |
|
| `s` | Open symbol picker (current document) |
|
||||||
| `w` | Enter [window mode](#window-mode) |
|
| `w` | Enter [window mode](#window-mode) |
|
||||||
| `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 |
|
||||||
|
| `p` | paste system clipboard after selections |
|
||||||
|
| `P` | paste system clipboard before selections |
|
||||||
|
| `y` | join and yank selections to clipboard |
|
||||||
|
| `Y` | yank main selection to clipboard |
|
||||||
|
| `R` | replace selections by clipboard contents |
|
||||||
|
|
||||||
# Picker
|
# Picker
|
||||||
|
|
||||||
@ -184,4 +188,4 @@ # Picker
|
|||||||
| `Enter` | Open selected |
|
| `Enter` | Open selected |
|
||||||
| `Ctrl-h` | Open horizontally |
|
| `Ctrl-h` | Open horizontally |
|
||||||
| `Ctrl-v` | Open vertically |
|
| `Ctrl-v` | Open vertically |
|
||||||
| `Escape`, `ctrl-c` | Close picker |
|
| `Escape`, `Ctrl-c` | Close picker |
|
||||||
|
@ -22,27 +22,29 @@ # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
|
|||||||
Control, Shift and Alt modifiers are encoded respectively with the prefixes
|
Control, Shift and Alt modifiers are encoded respectively with the prefixes
|
||||||
`C-`, `S-` and `A-`. Special keys are encoded as follows:
|
`C-`, `S-` and `A-`. Special keys are encoded as follows:
|
||||||
|
|
||||||
* Backspace => "backspace"
|
| Key name | Representation |
|
||||||
* Space => "space"
|
| --- | --- |
|
||||||
* Return/Enter => "ret"
|
| Backspace | `"backspace"` |
|
||||||
* < => "lt"
|
| Space | `"space"` |
|
||||||
* \> => "gt"
|
| Return/Enter | `"ret"` |
|
||||||
* \+ => "plus"
|
| < | `"lt"` |
|
||||||
* \- => "minus"
|
| \> | `"gt"` |
|
||||||
* ; => "semicolon"
|
| \+ | `"plus"` |
|
||||||
* % => "percent"
|
| \- | `"minus"` |
|
||||||
* Left => "left"
|
| ; | `"semicolon"` |
|
||||||
* Right => "right"
|
| % | `"percent"` |
|
||||||
* Up => "up"
|
| Left | `"left"` |
|
||||||
* Home => "home"
|
| Right | `"right"` |
|
||||||
* End => "end"
|
| Up | `"up"` |
|
||||||
* Page Up => "pageup"
|
| Home | `"home"` |
|
||||||
* Page Down => "pagedown"
|
| End | `"end"` |
|
||||||
* Tab => "tab"
|
| Page | `"pageup"` |
|
||||||
* Back Tab => "backtab"
|
| Page | `"pagedown"` |
|
||||||
* Delete => "del"
|
| Tab | `"tab"` |
|
||||||
* Insert => "ins"
|
| Back | `"backtab"` |
|
||||||
* Null => "null"
|
| Delete | `"del"` |
|
||||||
* Escape => "esc"
|
| Insert | `"ins"` |
|
||||||
|
| Null | `"null"` |
|
||||||
|
| Escape | `"esc"` |
|
||||||
|
|
||||||
Commands can be found in the source code at `../../helix-term/src/commands.rs`
|
Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
|
||||||
|
94
book/src/themes.md
Normal file
94
book/src/themes.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Themes
|
||||||
|
|
||||||
|
First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
|
||||||
|
|
||||||
|
To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
|
||||||
|
|
||||||
|
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/runtime/themes).
|
||||||
|
|
||||||
|
## Creating a theme
|
||||||
|
|
||||||
|
First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
|
||||||
|
|
||||||
|
Each line in the theme file is specified as below:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
where `key` 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` | For lifetimes |
|
||||||
|
| `module` | |
|
||||||
|
| `ui.background` | |
|
||||||
|
| `ui.linenr` | |
|
||||||
|
| `ui.statusline` | |
|
||||||
|
| `ui.popup` | |
|
||||||
|
| `ui.window` | |
|
||||||
|
| `ui.help` | |
|
||||||
|
| `ui.text` | |
|
||||||
|
| `ui.text.focus` | |
|
||||||
|
| `ui.menu.selected` | |
|
||||||
|
| `ui.selection` | For selections in the editing area |
|
||||||
|
| `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.
|
1
contrib/themes
Symbolic link
1
contrib/themes
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../runtime/themes
|
@ -19,12 +19,13 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
|
|||||||
ropey = "1.3"
|
ropey = "1.3"
|
||||||
smallvec = "1.4"
|
smallvec = "1.4"
|
||||||
tendril = "0.4.2"
|
tendril = "0.4.2"
|
||||||
unicode-segmentation = "1.7.1"
|
unicode-segmentation = "1.7"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
unicode-general-category = "0.4.0"
|
unicode-general-category = "0.4"
|
||||||
# slab = "0.4.2"
|
# slab = "0.4.2"
|
||||||
tree-sitter = "0.19"
|
tree-sitter = "0.19"
|
||||||
once_cell = "1.8"
|
once_cell = "1.8"
|
||||||
|
arc-swap = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -254,26 +254,23 @@ pub fn change<I>(document: &Document, changes: I) -> Self
|
|||||||
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
let loader = Loader::new(
|
let loader = Loader::new(Configuration {
|
||||||
Configuration {
|
language: vec![LanguageConfiguration {
|
||||||
language: vec![LanguageConfiguration {
|
scope: "source.rust".to_string(),
|
||||||
scope: "source.rust".to_string(),
|
file_types: vec!["rs".to_string()],
|
||||||
file_types: vec!["rs".to_string()],
|
language_id: Lang::Rust,
|
||||||
language_id: Lang::Rust,
|
highlight_config: OnceCell::new(),
|
||||||
highlight_config: OnceCell::new(),
|
//
|
||||||
//
|
roots: vec![],
|
||||||
roots: vec![],
|
auto_format: false,
|
||||||
auto_format: false,
|
language_server: None,
|
||||||
language_server: None,
|
indent: Some(IndentationConfiguration {
|
||||||
indent: Some(IndentationConfiguration {
|
tab_width: 4,
|
||||||
tab_width: 4,
|
unit: String::from(" "),
|
||||||
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"));
|
||||||
|
@ -19,6 +19,12 @@
|
|||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
|
||||||
|
pub mod unicode {
|
||||||
|
pub use unicode_general_category as category;
|
||||||
|
pub use unicode_segmentation as segmentation;
|
||||||
|
pub use unicode_width as width;
|
||||||
|
}
|
||||||
|
|
||||||
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
||||||
once_cell::sync::Lazy::new(runtime_dir);
|
once_cell::sync::Lazy::new(runtime_dir);
|
||||||
|
|
||||||
@ -51,7 +57,7 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(embed_runtime))]
|
#[cfg(not(embed_runtime))]
|
||||||
fn runtime_dir() -> std::path::PathBuf {
|
pub fn runtime_dir() -> std::path::PathBuf {
|
||||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||||
return dir.into();
|
return dir.into();
|
||||||
}
|
}
|
||||||
@ -98,8 +104,6 @@ pub fn cache_dir() -> std::path::PathBuf {
|
|||||||
|
|
||||||
pub use tendril::StrTendril as Tendril;
|
pub use tendril::StrTendril as Tendril;
|
||||||
|
|
||||||
pub use unicode_general_category::get_general_category;
|
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use {regex, tree_sitter};
|
pub use {regex, tree_sitter};
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
|
use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
|
||||||
pub use helix_syntax::{get_language, get_language_name, Lang};
|
pub use helix_syntax::{get_language, get_language_name, Lang};
|
||||||
|
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
@ -143,37 +145,49 @@ fn read_query(language: &str, filename: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageConfiguration {
|
impl LanguageConfiguration {
|
||||||
|
fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
|
||||||
|
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
||||||
|
|
||||||
|
let highlights_query = read_query(&language, "highlights.scm");
|
||||||
|
// always highlight syntax errors
|
||||||
|
// highlights_query += "\n(ERROR) @error";
|
||||||
|
|
||||||
|
let injections_query = read_query(&language, "injections.scm");
|
||||||
|
|
||||||
|
let locals_query = "";
|
||||||
|
|
||||||
|
if highlights_query.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let language = get_language(self.language_id);
|
||||||
|
let mut config = HighlightConfiguration::new(
|
||||||
|
language,
|
||||||
|
&highlights_query,
|
||||||
|
&injections_query,
|
||||||
|
locals_query,
|
||||||
|
)
|
||||||
|
.unwrap(); // TODO: no unwrap
|
||||||
|
config.configure(scopes);
|
||||||
|
Some(Arc::new(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reconfigure(&self, scopes: &[String]) {
|
||||||
|
if let Some(Some(config)) = self.highlight_config.get() {
|
||||||
|
config.configure(scopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
|
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
|
||||||
self.highlight_config
|
self.highlight_config
|
||||||
.get_or_init(|| {
|
.get_or_init(|| self.initialize_highlight(scopes))
|
||||||
let language = get_language_name(self.language_id).to_ascii_lowercase();
|
|
||||||
|
|
||||||
let highlights_query = read_query(&language, "highlights.scm");
|
|
||||||
// always highlight syntax errors
|
|
||||||
// highlights_query += "\n(ERROR) @error";
|
|
||||||
|
|
||||||
let injections_query = read_query(&language, "injections.scm");
|
|
||||||
|
|
||||||
let locals_query = "";
|
|
||||||
|
|
||||||
if highlights_query.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let language = get_language(self.language_id);
|
|
||||||
let mut config = HighlightConfiguration::new(
|
|
||||||
language,
|
|
||||||
&highlights_query,
|
|
||||||
&injections_query,
|
|
||||||
locals_query,
|
|
||||||
)
|
|
||||||
.unwrap(); // TODO: no unwrap
|
|
||||||
config.configure(scopes);
|
|
||||||
Some(Arc::new(config))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_highlight_initialized(&self) -> bool {
|
||||||
|
self.highlight_config.get().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn indent_query(&self) -> Option<&IndentQuery> {
|
pub fn indent_query(&self) -> Option<&IndentQuery> {
|
||||||
self.indent_query
|
self.indent_query
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
@ -190,22 +204,18 @@ pub fn scope(&self) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static LOADER: OnceCell<Loader> = OnceCell::new();
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Loader {
|
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, scopes: Vec<String>) -> Self {
|
pub fn new(config: Configuration) -> 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 {
|
||||||
@ -225,10 +235,6 @@ pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
|
|||||||
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.
|
||||||
@ -253,6 +259,10 @@ pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfi
|
|||||||
.find(|config| config.scope == scope)
|
.find(|config| config.scope == scope)
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
|
||||||
|
self.language_configs.iter()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TsParser {
|
pub struct TsParser {
|
||||||
@ -772,7 +782,7 @@ pub struct HighlightConfiguration {
|
|||||||
combined_injections_query: Option<Query>,
|
combined_injections_query: Option<Query>,
|
||||||
locals_pattern_index: usize,
|
locals_pattern_index: usize,
|
||||||
highlights_pattern_index: usize,
|
highlights_pattern_index: usize,
|
||||||
highlight_indices: Vec<Option<Highlight>>,
|
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
|
||||||
non_local_variable_patterns: Vec<bool>,
|
non_local_variable_patterns: Vec<bool>,
|
||||||
injection_content_capture_index: Option<u32>,
|
injection_content_capture_index: Option<u32>,
|
||||||
injection_language_capture_index: Option<u32>,
|
injection_language_capture_index: Option<u32>,
|
||||||
@ -924,7 +934,7 @@ pub fn new(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlight_indices = vec![None; query.capture_names().len()];
|
let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
language,
|
language,
|
||||||
query,
|
query,
|
||||||
@ -957,17 +967,20 @@ pub fn names(&self) -> &[String] {
|
|||||||
///
|
///
|
||||||
/// When highlighting, results are returned as `Highlight` values, which contain the index
|
/// When highlighting, results are returned as `Highlight` values, which contain the index
|
||||||
/// of the matched highlight this list of highlight names.
|
/// of the matched highlight this list of highlight names.
|
||||||
pub fn configure(&mut self, recognized_names: &[String]) {
|
pub fn configure(&self, recognized_names: &[String]) {
|
||||||
let mut capture_parts = Vec::new();
|
let mut capture_parts = Vec::new();
|
||||||
self.highlight_indices.clear();
|
let indices: Vec<_> = self
|
||||||
self.highlight_indices
|
.query
|
||||||
.extend(self.query.capture_names().iter().map(move |capture_name| {
|
.capture_names()
|
||||||
|
.iter()
|
||||||
|
.map(move |capture_name| {
|
||||||
capture_parts.clear();
|
capture_parts.clear();
|
||||||
capture_parts.extend(capture_name.split('.'));
|
capture_parts.extend(capture_name.split('.'));
|
||||||
|
|
||||||
let mut best_index = None;
|
let mut best_index = None;
|
||||||
let mut best_match_len = 0;
|
let mut best_match_len = 0;
|
||||||
for (i, recognized_name) in recognized_names.iter().enumerate() {
|
for (i, recognized_name) in recognized_names.iter().enumerate() {
|
||||||
|
let recognized_name = recognized_name;
|
||||||
let mut len = 0;
|
let mut len = 0;
|
||||||
let mut matches = true;
|
let mut matches = true;
|
||||||
for part in recognized_name.split('.') {
|
for part in recognized_name.split('.') {
|
||||||
@ -983,7 +996,10 @@ pub fn configure(&mut self, recognized_names: &[String]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
best_index.map(Highlight)
|
best_index.map(Highlight)
|
||||||
}));
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.highlight_indices.store(Arc::new(indices));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1562,7 +1578,7 @@ fn next(&mut self) -> Option<Self::Item> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_highlight = layer.config.highlight_indices[capture.index as usize];
|
let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
|
||||||
|
|
||||||
// If this node represents a local definition, then store the current
|
// If this node represents a local definition, then store the current
|
||||||
// highlight value on the local scope entry representing this node.
|
// highlight value on the local scope entry representing this node.
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
use helix_core::syntax;
|
||||||
use helix_lsp::{lsp, LspProgressMap};
|
use helix_lsp::{lsp, LspProgressMap};
|
||||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
use helix_view::{document::Mode, theme, Document, Editor, Theme, View};
|
||||||
|
|
||||||
use crate::{args::Args, compositor::Compositor, config::Config, ui};
|
use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
|
||||||
|
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
|
||||||
@ -14,7 +15,7 @@
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::{Context, Error};
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{Event, EventStream},
|
event::{Event, EventStream},
|
||||||
@ -36,6 +37,8 @@ pub struct Application {
|
|||||||
compositor: Compositor,
|
compositor: Compositor,
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
|
|
||||||
|
theme_loader: Arc<theme::Loader>,
|
||||||
|
syn_loader: Arc<syntax::Loader>,
|
||||||
callbacks: LspCallbacks,
|
callbacks: LspCallbacks,
|
||||||
|
|
||||||
lsp_progress: LspProgressMap,
|
lsp_progress: LspProgressMap,
|
||||||
@ -47,9 +50,36 @@ pub fn new(mut args: Args, config: Config) -> Result<Self, Error> {
|
|||||||
use helix_view::editor::Action;
|
use helix_view::editor::Action;
|
||||||
let mut compositor = Compositor::new()?;
|
let mut compositor = Compositor::new()?;
|
||||||
let size = compositor.size();
|
let size = compositor.size();
|
||||||
let mut editor = Editor::new(size);
|
|
||||||
|
|
||||||
let mut editor_view = Box::new(ui::EditorView::new(config.keys));
|
let conf_dir = helix_core::config_dir();
|
||||||
|
|
||||||
|
let theme_loader =
|
||||||
|
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
|
||||||
|
|
||||||
|
// load $HOME/.config/helix/languages.toml, fallback to default config
|
||||||
|
let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
|
||||||
|
let lang_conf = lang_conf
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(include_bytes!("../../languages.toml"));
|
||||||
|
|
||||||
|
let theme = if let Some(theme) = &config.global.theme {
|
||||||
|
match theme_loader.load(theme) {
|
||||||
|
Ok(theme) => theme,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to load theme `{}` - {}", theme, e);
|
||||||
|
theme_loader.default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme_loader.default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
|
||||||
|
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
|
||||||
|
|
||||||
|
let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone());
|
||||||
|
|
||||||
|
let mut editor_view = Box::new(ui::EditorView::new(config.keymaps));
|
||||||
compositor.push(editor_view);
|
compositor.push(editor_view);
|
||||||
|
|
||||||
if !args.files.is_empty() {
|
if !args.files.is_empty() {
|
||||||
@ -72,10 +102,14 @@ pub fn new(mut args: Args, config: Config) -> Result<Self, Error> {
|
|||||||
editor.new_file(Action::VerticalSplit);
|
editor.new_file(Action::VerticalSplit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor.set_theme(theme);
|
||||||
|
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
compositor,
|
compositor,
|
||||||
editor,
|
editor,
|
||||||
|
|
||||||
|
theme_loader,
|
||||||
|
syn_loader,
|
||||||
callbacks: FuturesUnordered::new(),
|
callbacks: FuturesUnordered::new(),
|
||||||
lsp_progress: LspProgressMap::new(),
|
lsp_progress: LspProgressMap::new(),
|
||||||
lsp_progress_enabled: config.global.lsp_progress,
|
lsp_progress_enabled: config.global.lsp_progress,
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
document::{IndentStyle, Mode},
|
document::{IndentStyle, Mode},
|
||||||
input::{KeyCode, KeyEvent},
|
|
||||||
view::{View, PADDING},
|
view::{View, PADDING},
|
||||||
Document, DocumentId, Editor, ViewId,
|
Document, DocumentId, Editor, ViewId,
|
||||||
};
|
};
|
||||||
@ -39,8 +38,8 @@
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::de::{self, Deserialize, Deserializer};
|
|
||||||
|
|
||||||
pub struct Context<'a> {
|
pub struct Context<'a> {
|
||||||
pub selected_register: helix_view::RegisterSelection,
|
pub selected_register: helix_view::RegisterSelection,
|
||||||
@ -186,7 +185,6 @@ pub fn name(&self) -> &'static str {
|
|||||||
search_next,
|
search_next,
|
||||||
extend_search_next,
|
extend_search_next,
|
||||||
search_selection,
|
search_selection,
|
||||||
select_line,
|
|
||||||
extend_line,
|
extend_line,
|
||||||
delete_selection,
|
delete_selection,
|
||||||
change_selection,
|
change_selection,
|
||||||
@ -223,9 +221,14 @@ pub fn name(&self) -> &'static str {
|
|||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
yank,
|
yank,
|
||||||
|
yank_joined_to_clipboard,
|
||||||
|
yank_main_selection_to_clipboard,
|
||||||
replace_with_yanked,
|
replace_with_yanked,
|
||||||
|
replace_selections_with_clipboard,
|
||||||
paste_after,
|
paste_after,
|
||||||
paste_before,
|
paste_before,
|
||||||
|
paste_clipboard_after,
|
||||||
|
paste_clipboard_before,
|
||||||
indent,
|
indent,
|
||||||
unindent,
|
unindent,
|
||||||
format_selections,
|
format_selections,
|
||||||
@ -253,48 +256,6 @@ pub fn name(&self) -> &'static str {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Command {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let Command(name, _) = self;
|
|
||||||
f.debug_tuple("Command").field(name).finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Command {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let Command(name, _) = self;
|
|
||||||
f.write_str(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Command {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Command::COMMAND_LIST
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.find(|cmd| cmd.0 == s)
|
|
||||||
.ok_or_else(|| anyhow!("No command named '{}'", s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Command {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
s.parse().map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Command {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.name() == other.name()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_char_left(cx: &mut Context) {
|
fn move_char_left(cx: &mut Context) {
|
||||||
let count = cx.count();
|
let count = cx.count();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
@ -926,21 +887,6 @@ fn search_selection(cx: &mut Context) {
|
|||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
fn select_line(cx: &mut Context) {
|
|
||||||
let count = cx.count();
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
|
|
||||||
let pos = doc.selection(view.id).primary();
|
|
||||||
let text = doc.text();
|
|
||||||
|
|
||||||
let line = text.char_to_line(pos.head);
|
|
||||||
let start = text.line_to_char(line);
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
fn extend_line(cx: &mut Context) {
|
fn extend_line(cx: &mut Context) {
|
||||||
let count = cx.count();
|
let count = cx.count();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
@ -1318,6 +1264,57 @@ fn force_quit_all(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
|||||||
quit_all_impl(editor, args, event, true)
|
quit_all_impl(editor, args, event, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn theme(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||||
|
let theme = if let Some(theme) = args.first() {
|
||||||
|
theme
|
||||||
|
} else {
|
||||||
|
editor.set_error("theme name not provided".into());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.set_theme_from_name(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yank_main_selection_to_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
yank_main_selection_to_clipboard_impl(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], _: PromptEvent) {
|
||||||
|
let separator = args.first().copied().unwrap_or("\n");
|
||||||
|
yank_joined_to_clipboard_impl(editor, separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_after(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
paste_clipboard_impl(editor, Paste::After);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_before(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
paste_clipboard_impl(editor, Paste::After);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_selections_with_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
match editor.clipboard_provider.get_contents() {
|
||||||
|
Ok(contents) => {
|
||||||
|
let transaction =
|
||||||
|
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
|
||||||
|
let max_to = doc.text().len_chars().saturating_sub(1);
|
||||||
|
let to = std::cmp::min(max_to, range.to() + 1);
|
||||||
|
(range.from(), to, Some(contents.as_str().into()))
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.apply(&transaction, view.id);
|
||||||
|
doc.append_changes_to_history(view.id);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_clipboard_provider(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
editor.set_status(editor.clipboard_provider.name().into());
|
||||||
|
}
|
||||||
|
|
||||||
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "quit",
|
name: "quit",
|
||||||
@ -1431,7 +1428,55 @@ fn force_quit_all(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
|||||||
fun: force_quit_all,
|
fun: force_quit_all,
|
||||||
completer: None,
|
completer: None,
|
||||||
},
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "theme",
|
||||||
|
alias: None,
|
||||||
|
doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
|
||||||
|
fun: theme,
|
||||||
|
completer: Some(completers::theme),
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-yank",
|
||||||
|
alias: None,
|
||||||
|
doc: "Yank main selection into system clipboard.",
|
||||||
|
fun: yank_main_selection_to_clipboard,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-yank-join",
|
||||||
|
alias: None,
|
||||||
|
doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
|
||||||
|
fun: yank_joined_to_clipboard,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-paste-after",
|
||||||
|
alias: None,
|
||||||
|
doc: "Paste system clipboard after selections.",
|
||||||
|
fun: paste_clipboard_after,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-paste-before",
|
||||||
|
alias: None,
|
||||||
|
doc: "Paste system clipboard before selections.",
|
||||||
|
fun: paste_clipboard_before,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-paste-replace",
|
||||||
|
alias: None,
|
||||||
|
doc: "Replace selections with content of system clipboard.",
|
||||||
|
fun: replace_selections_with_clipboard,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "show-clipboard-provider",
|
||||||
|
alias: None,
|
||||||
|
doc: "Show clipboard provider name in status bar.",
|
||||||
|
fun: show_clipboard_provider,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
|
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
|
||||||
@ -2424,6 +2469,52 @@ fn yank(cx: &mut Context) {
|
|||||||
cx.editor.set_status(msg)
|
cx.editor.set_status(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
let values: Vec<String> = doc
|
||||||
|
.selection(view.id)
|
||||||
|
.fragments(doc.text().slice(..))
|
||||||
|
.map(Cow::into_owned)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let msg = format!(
|
||||||
|
"joined and yanked {} selection(s) to system clipboard",
|
||||||
|
values.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let joined = values.join(separator);
|
||||||
|
|
||||||
|
if let Err(e) = editor.clipboard_provider.set_contents(joined) {
|
||||||
|
log::error!("Couldn't set system clipboard content: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.set_status(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yank_joined_to_clipboard(cx: &mut Context) {
|
||||||
|
yank_joined_to_clipboard_impl(&mut cx.editor, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
let value = doc
|
||||||
|
.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.fragment(doc.text().slice(..));
|
||||||
|
|
||||||
|
if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
|
||||||
|
log::error!("Couldn't set system clipboard content: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.set_status("yanked main selection to system clipboard".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yank_main_selection_to_clipboard(cx: &mut Context) {
|
||||||
|
yank_main_selection_to_clipboard_impl(&mut cx.editor);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
enum Paste {
|
enum Paste {
|
||||||
Before,
|
Before,
|
||||||
@ -2469,6 +2560,31 @@ fn paste_impl(
|
|||||||
Some(transaction)
|
Some(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_impl(editor: &mut Editor, action: Paste) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
match editor
|
||||||
|
.clipboard_provider
|
||||||
|
.get_contents()
|
||||||
|
.map(|contents| paste_impl(&[contents], doc, view, action))
|
||||||
|
{
|
||||||
|
Ok(Some(transaction)) => {
|
||||||
|
doc.apply(&transaction, view.id);
|
||||||
|
doc.append_changes_to_history(view.id);
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_after(cx: &mut Context) {
|
||||||
|
paste_clipboard_impl(&mut cx.editor, Paste::After);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_before(cx: &mut Context) {
|
||||||
|
paste_clipboard_impl(&mut cx.editor, Paste::Before);
|
||||||
|
}
|
||||||
|
|
||||||
fn replace_with_yanked(cx: &mut Context) {
|
fn replace_with_yanked(cx: &mut Context) {
|
||||||
let reg_name = cx.selected_register.name();
|
let reg_name = cx.selected_register.name();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
@ -2489,6 +2605,29 @@ fn replace_with_yanked(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_selections_with_clipboard_impl(editor: &mut Editor) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
match editor.clipboard_provider.get_contents() {
|
||||||
|
Ok(contents) => {
|
||||||
|
let transaction =
|
||||||
|
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
|
||||||
|
let max_to = doc.text().len_chars().saturating_sub(1);
|
||||||
|
let to = std::cmp::min(max_to, range.to() + 1);
|
||||||
|
(range.from(), to, Some(contents.as_str().into()))
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.apply(&transaction, view.id);
|
||||||
|
doc.append_changes_to_history(view.id);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_selections_with_clipboard(cx: &mut Context) {
|
||||||
|
replace_selections_with_clipboard_impl(&mut cx.editor);
|
||||||
|
}
|
||||||
|
|
||||||
// alt-p => paste every yanked selection after selected text
|
// alt-p => paste every yanked selection after selected text
|
||||||
// alt-P => paste every yanked selection before selected text
|
// alt-P => paste every yanked selection before selected text
|
||||||
// R => replace selected text with yanked text
|
// R => replace selected text with yanked text
|
||||||
@ -2854,7 +2993,7 @@ fn hover(cx: &mut Context) {
|
|||||||
|
|
||||||
// skip if contents empty
|
// skip if contents empty
|
||||||
|
|
||||||
let contents = ui::Markdown::new(contents);
|
let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
|
||||||
let mut popup = Popup::new(contents);
|
let mut popup = Popup::new(contents);
|
||||||
compositor.push(Box::new(popup));
|
compositor.push(Box::new(popup));
|
||||||
}
|
}
|
||||||
@ -3009,6 +3148,11 @@ fn space_mode(cx: &mut Context) {
|
|||||||
'b' => buffer_picker(cx),
|
'b' => buffer_picker(cx),
|
||||||
's' => symbol_picker(cx),
|
's' => symbol_picker(cx),
|
||||||
'w' => window_mode(cx),
|
'w' => window_mode(cx),
|
||||||
|
'y' => yank_joined_to_clipboard(cx),
|
||||||
|
'Y' => yank_main_selection_to_clipboard(cx),
|
||||||
|
'p' => paste_clipboard_after(cx),
|
||||||
|
'P' => paste_clipboard_before(cx),
|
||||||
|
'R' => replace_selections_with_clipboard(cx),
|
||||||
// ' ' => toggle_alternate_buffer(cx),
|
// ' ' => toggle_alternate_buffer(cx),
|
||||||
// TODO: temporary since space mode took its old key
|
// TODO: temporary since space mode took its old key
|
||||||
' ' => keep_primary_selection(cx),
|
' ' => keep_primary_selection(cx),
|
||||||
@ -3092,3 +3236,29 @@ fn right_bracket_mode(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Command {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Command(name, _) = self;
|
||||||
|
f.write_str(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Command {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Command::COMMAND_LIST
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|cmd| cmd.0 == s)
|
||||||
|
.ok_or_else(|| anyhow!("No command named '{}'", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Command {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Command(name, _) = self;
|
||||||
|
f.debug_tuple("Command").field(name).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -178,13 +178,13 @@ pub trait AnyComponent {
|
|||||||
/// Returns a boxed any from a boxed self.
|
/// Returns a boxed any from a boxed self.
|
||||||
///
|
///
|
||||||
/// Can be used before `Box::downcast()`.
|
/// Can be used before `Box::downcast()`.
|
||||||
///
|
//
|
||||||
/// # Examples
|
// # Examples
|
||||||
///
|
//
|
||||||
/// ```rust
|
// ```rust
|
||||||
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||||
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||||
/// ```
|
// ```
|
||||||
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
|
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,63 +1,55 @@
|
|||||||
use serde::Deserialize;
|
use anyhow::{Error, Result};
|
||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use crate::commands::Command;
|
use serde::{de::Error as SerdeError, Deserialize, Serialize};
|
||||||
use crate::keymap::Keymaps;
|
|
||||||
|
use crate::keymap::{parse_keymaps, Keymaps};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
|
||||||
pub struct GlobalConfig {
|
pub struct GlobalConfig {
|
||||||
|
pub theme: Option<String>,
|
||||||
pub lsp_progress: bool,
|
pub lsp_progress: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GlobalConfig {
|
impl Default for GlobalConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { lsp_progress: true }
|
Self {
|
||||||
|
lsp_progress: true,
|
||||||
|
theme: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Deserialize)]
|
#[derive(Default)]
|
||||||
#[serde(default)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub global: GlobalConfig,
|
pub global: GlobalConfig,
|
||||||
pub keys: Keymaps,
|
pub keymaps: Keymaps,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[derive(Serialize, Deserialize)]
|
||||||
fn parsing_keymaps_config_file() {
|
#[serde(rename_all = "kebab-case")]
|
||||||
use helix_core::hashmap;
|
struct TomlConfig {
|
||||||
use helix_view::document::Mode;
|
theme: Option<String>,
|
||||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
lsp_progress: Option<bool>,
|
||||||
|
keys: Option<HashMap<String, HashMap<String, String>>>,
|
||||||
let sample_keymaps = r#"
|
}
|
||||||
[keys.insert]
|
|
||||||
y = "move_line_down"
|
impl<'de> Deserialize<'de> for Config {
|
||||||
S-C-a = "delete_selection"
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
[keys.normal]
|
D: serde::Deserializer<'de>,
|
||||||
A-F12 = "move_next_word_end"
|
{
|
||||||
"#;
|
let config = TomlConfig::deserialize(deserializer)?;
|
||||||
|
Ok(Self {
|
||||||
assert_eq!(
|
global: GlobalConfig {
|
||||||
toml::from_str::<Config>(sample_keymaps).unwrap(),
|
lsp_progress: config.lsp_progress.unwrap_or(true),
|
||||||
Config {
|
theme: config.theme,
|
||||||
global: Default::default(),
|
},
|
||||||
keys: Keymaps(hashmap! {
|
keymaps: config
|
||||||
Mode::Insert => hashmap! {
|
.keys
|
||||||
KeyEvent {
|
.map(|r| parse_keymaps(&r))
|
||||||
code: KeyCode::Char('y'),
|
.transpose()
|
||||||
modifiers: KeyModifiers::NONE,
|
.map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))?
|
||||||
} => Command::move_line_down,
|
.unwrap_or_else(Keymaps::default),
|
||||||
KeyEvent {
|
})
|
||||||
code: KeyCode::Char('a'),
|
}
|
||||||
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
|
|
||||||
} => Command::delete_selection,
|
|
||||||
},
|
|
||||||
Mode::Normal => hashmap! {
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::F(12),
|
|
||||||
modifiers: KeyModifiers::ALT,
|
|
||||||
} => Command::move_next_word_end,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use helix_core::hashmap;
|
use helix_core::hashmap;
|
||||||
use helix_view::document::Mode;
|
use helix_view::document::Mode;
|
||||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
@ -101,6 +99,14 @@
|
|||||||
// D] = last diagnostic
|
// D] = last diagnostic
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// #[cfg(feature = "term")]
|
||||||
|
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Keymap(pub HashMap<KeyEvent, Command>);
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Keymaps(pub HashMap<Mode, Keymap>);
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! key {
|
macro_rules! key {
|
||||||
($key:ident) => {
|
($key:ident) => {
|
||||||
@ -135,21 +141,9 @@ macro_rules! alt {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
|
|
||||||
|
|
||||||
impl Deref for Keymaps {
|
|
||||||
type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Keymaps {
|
impl Default for Keymaps {
|
||||||
fn default() -> Keymaps {
|
fn default() -> Self {
|
||||||
let normal = hashmap!(
|
let normal = Keymap(hashmap!(
|
||||||
key!('h') => Command::move_char_left,
|
key!('h') => Command::move_char_left,
|
||||||
key!('j') => Command::move_line_down,
|
key!('j') => Command::move_line_down,
|
||||||
key!('k') => Command::move_line_up,
|
key!('k') => Command::move_line_up,
|
||||||
@ -202,9 +196,7 @@ fn default() -> Keymaps {
|
|||||||
key!(';') => Command::collapse_selection,
|
key!(';') => Command::collapse_selection,
|
||||||
alt!(';') => Command::flip_selections,
|
alt!(';') => Command::flip_selections,
|
||||||
key!('%') => Command::select_all,
|
key!('%') => Command::select_all,
|
||||||
key!('x') => Command::select_line,
|
key!('x') => Command::extend_line,
|
||||||
key!('X') => Command::extend_line,
|
|
||||||
// or select mode X?
|
|
||||||
// extend_to_whole_line, crop_to_whole_line
|
// extend_to_whole_line, crop_to_whole_line
|
||||||
|
|
||||||
|
|
||||||
@ -283,12 +275,12 @@ fn default() -> Keymaps {
|
|||||||
key!('z') => Command::view_mode,
|
key!('z') => Command::view_mode,
|
||||||
|
|
||||||
key!('"') => Command::select_register,
|
key!('"') => Command::select_register,
|
||||||
);
|
));
|
||||||
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
|
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
|
||||||
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
|
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
|
||||||
// because some selection operations can now be done from normal mode, some from select mode.
|
// because some selection operations can now be done from normal mode, some from select mode.
|
||||||
let mut select = normal.clone();
|
let mut select = normal.clone();
|
||||||
select.extend(
|
select.0.extend(
|
||||||
hashmap!(
|
hashmap!(
|
||||||
key!('h') => Command::extend_char_left,
|
key!('h') => Command::extend_char_left,
|
||||||
key!('j') => Command::extend_line_down,
|
key!('j') => Command::extend_line_down,
|
||||||
@ -321,7 +313,7 @@ fn default() -> Keymaps {
|
|||||||
// TODO: select could be normal mode with some bindings merged over
|
// TODO: select could be normal mode with some bindings merged over
|
||||||
Mode::Normal => normal,
|
Mode::Normal => normal,
|
||||||
Mode::Select => select,
|
Mode::Select => select,
|
||||||
Mode::Insert => hashmap!(
|
Mode::Insert => Keymap(hashmap!(
|
||||||
key!(Esc) => Command::normal_mode as Command,
|
key!(Esc) => Command::normal_mode as Command,
|
||||||
key!(Backspace) => Command::delete_char_backward,
|
key!(Backspace) => Command::delete_char_backward,
|
||||||
key!(Delete) => Command::delete_char_forward,
|
key!(Delete) => Command::delete_char_forward,
|
||||||
@ -333,9 +325,313 @@ fn default() -> Keymaps {
|
|||||||
key!(Right) => Command::move_char_right,
|
key!(Right) => Command::move_char_right,
|
||||||
key!(PageUp) => Command::page_up,
|
key!(PageUp) => Command::page_up,
|
||||||
key!(PageDown) => Command::page_down,
|
key!(PageDown) => Command::page_down,
|
||||||
|
key!(Home) => Command::move_line_start,
|
||||||
|
key!(End) => Command::move_line_end,
|
||||||
ctrl!('x') => Command::completion,
|
ctrl!('x') => Command::completion,
|
||||||
ctrl!('w') => Command::delete_word_backward,
|
ctrl!('w') => Command::delete_word_backward,
|
||||||
),
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Newtype wrapper over keys to allow toml serialization/parsing
|
||||||
|
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)]
|
||||||
|
pub struct RepresentableKeyEvent(pub KeyEvent);
|
||||||
|
impl Display for RepresentableKeyEvent {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self(key) = self;
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{}{}{}",
|
||||||
|
if key.modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
"S-"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
if key.modifiers.contains(KeyModifiers::ALT) {
|
||||||
|
"A-"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
"C-"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
))?;
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Backspace => f.write_str("backspace")?,
|
||||||
|
KeyCode::Enter => f.write_str("ret")?,
|
||||||
|
KeyCode::Left => f.write_str("left")?,
|
||||||
|
KeyCode::Right => f.write_str("right")?,
|
||||||
|
KeyCode::Up => f.write_str("up")?,
|
||||||
|
KeyCode::Down => f.write_str("down")?,
|
||||||
|
KeyCode::Home => f.write_str("home")?,
|
||||||
|
KeyCode::End => f.write_str("end")?,
|
||||||
|
KeyCode::PageUp => f.write_str("pageup")?,
|
||||||
|
KeyCode::PageDown => f.write_str("pagedown")?,
|
||||||
|
KeyCode::Tab => f.write_str("tab")?,
|
||||||
|
KeyCode::BackTab => f.write_str("backtab")?,
|
||||||
|
KeyCode::Delete => f.write_str("del")?,
|
||||||
|
KeyCode::Insert => f.write_str("ins")?,
|
||||||
|
KeyCode::Null => f.write_str("null")?,
|
||||||
|
KeyCode::Esc => f.write_str("esc")?,
|
||||||
|
KeyCode::Char('<') => f.write_str("lt")?,
|
||||||
|
KeyCode::Char('>') => f.write_str("gt")?,
|
||||||
|
KeyCode::Char('+') => f.write_str("plus")?,
|
||||||
|
KeyCode::Char('-') => f.write_str("minus")?,
|
||||||
|
KeyCode::Char(';') => f.write_str("semicolon")?,
|
||||||
|
KeyCode::Char('%') => f.write_str("percent")?,
|
||||||
|
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
|
||||||
|
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RepresentableKeyEvent {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut tokens: Vec<_> = s.split('-').collect();
|
||||||
|
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
|
||||||
|
"backspace" => KeyCode::Backspace,
|
||||||
|
"space" => KeyCode::Char(' '),
|
||||||
|
"ret" => KeyCode::Enter,
|
||||||
|
"lt" => KeyCode::Char('<'),
|
||||||
|
"gt" => KeyCode::Char('>'),
|
||||||
|
"plus" => KeyCode::Char('+'),
|
||||||
|
"minus" => KeyCode::Char('-'),
|
||||||
|
"semicolon" => KeyCode::Char(';'),
|
||||||
|
"percent" => KeyCode::Char('%'),
|
||||||
|
"left" => KeyCode::Left,
|
||||||
|
"right" => KeyCode::Right,
|
||||||
|
"up" => KeyCode::Down,
|
||||||
|
"home" => KeyCode::Home,
|
||||||
|
"end" => KeyCode::End,
|
||||||
|
"pageup" => KeyCode::PageUp,
|
||||||
|
"pagedown" => KeyCode::PageDown,
|
||||||
|
"tab" => KeyCode::Tab,
|
||||||
|
"backtab" => KeyCode::BackTab,
|
||||||
|
"del" => KeyCode::Delete,
|
||||||
|
"ins" => KeyCode::Insert,
|
||||||
|
"null" => KeyCode::Null,
|
||||||
|
"esc" => KeyCode::Esc,
|
||||||
|
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
|
||||||
|
function if function.len() > 1 && function.starts_with('F') => {
|
||||||
|
let function: String = function.chars().skip(1).collect();
|
||||||
|
let function = str::parse::<u8>(&function)?;
|
||||||
|
(function > 0 && function < 13)
|
||||||
|
.then(|| KeyCode::F(function))
|
||||||
|
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
|
||||||
|
}
|
||||||
|
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut modifiers = KeyModifiers::empty();
|
||||||
|
for token in tokens {
|
||||||
|
let flag = match token {
|
||||||
|
"S" => KeyModifiers::SHIFT,
|
||||||
|
"A" => KeyModifiers::ALT,
|
||||||
|
"C" => KeyModifiers::CONTROL,
|
||||||
|
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if modifiers.contains(flag) {
|
||||||
|
return Err(anyhow!("Repeated key modifier '{}-'", token));
|
||||||
|
}
|
||||||
|
modifiers.insert(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RepresentableKeyEvent(KeyEvent { code, modifiers }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_keymaps(toml_keymaps: &HashMap<String, HashMap<String, String>>) -> Result<Keymaps> {
|
||||||
|
let mut keymaps = Keymaps::default();
|
||||||
|
|
||||||
|
for (mode, map) in toml_keymaps {
|
||||||
|
let mode = Mode::from_str(&mode)?;
|
||||||
|
for (key, command) in map {
|
||||||
|
let key = str::parse::<RepresentableKeyEvent>(&key)?;
|
||||||
|
let command = str::parse::<Command>(&command)?;
|
||||||
|
keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(keymaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Keymap {
|
||||||
|
type Target = HashMap<KeyEvent, Command>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Keymaps {
|
||||||
|
type Target = HashMap<Mode, Keymap>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Keymap {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Keymaps {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl PartialEq for Command {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name() == other.name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_keymaps_config_file() {
|
||||||
|
let sample_keymaps = r#"
|
||||||
|
[keys.insert]
|
||||||
|
y = "move_line_down"
|
||||||
|
S-C-a = "delete_selection"
|
||||||
|
|
||||||
|
[keys.normal]
|
||||||
|
A-F12 = "move_next_word_end"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config: Config = toml::from_str(sample_keymaps).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
*config
|
||||||
|
.keymaps
|
||||||
|
.0
|
||||||
|
.get(&Mode::Insert)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.get(&KeyEvent {
|
||||||
|
code: KeyCode::Char('y'),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
Command::move_line_down
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*config
|
||||||
|
.keymaps
|
||||||
|
.0
|
||||||
|
.get(&Mode::Insert)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.get(&KeyEvent {
|
||||||
|
code: KeyCode::Char('a'),
|
||||||
|
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
Command::delete_selection
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*config
|
||||||
|
.keymaps
|
||||||
|
.0
|
||||||
|
.get(&Mode::Normal)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.get(&KeyEvent {
|
||||||
|
code: KeyCode::F(12),
|
||||||
|
modifiers: KeyModifiers::ALT
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
Command::move_next_word_end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_unmodified_keys() {
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("backspace").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::Backspace,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("left").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>(",").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::Char(','),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("w").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::Char('w'),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("F12").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::F(12),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parsing_modified_keys() {
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("S-minus").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::Char('-'),
|
||||||
|
modifiers: KeyModifiers::SHIFT
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("C-A-S-F12").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::F(12),
|
||||||
|
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<RepresentableKeyEvent>("S-C-2").unwrap(),
|
||||||
|
RepresentableKeyEvent(KeyEvent {
|
||||||
|
code: KeyCode::F(2),
|
||||||
|
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_nonsensical_keys_fails() {
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("F13").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("F0").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("aaa").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("S-S-a").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("C-A-S-C-1").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("FU").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("123").is_err());
|
||||||
|
assert!(str::parse::<RepresentableKeyEvent>("S--").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use anyhow::{Context, Error, Result};
|
|
||||||
use helix_term::application::Application;
|
use helix_term::application::Application;
|
||||||
use helix_term::args::Args;
|
use helix_term::args::Args;
|
||||||
use helix_term::config::Config;
|
use helix_term::config::Config;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
fn setup_logging(logpath: PathBuf, 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();
|
||||||
|
|
||||||
@ -88,11 +89,12 @@ async fn main() -> Result<()> {
|
|||||||
std::fs::create_dir_all(&conf_dir).ok();
|
std::fs::create_dir_all(&conf_dir).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
|
let config = std::fs::read_to_string(conf_dir.join("config.toml"))
|
||||||
Ok(config) => toml::from_str(&config)?,
|
.ok()
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
|
.map(|s| toml::from_str(&s))
|
||||||
Err(err) => return Err(Error::new(err)),
|
.transpose()?
|
||||||
};
|
.or_else(|| Some(Config::default()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||||
|
|
||||||
|
@ -238,6 +238,9 @@ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
.language()
|
.language()
|
||||||
.and_then(|scope| scope.strip_prefix("source."))
|
.and_then(|scope| scope.strip_prefix("source."))
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
let cursor_pos = doc.selection(view.id).cursor();
|
||||||
|
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
|
||||||
|
- view.first_line) as u16;
|
||||||
|
|
||||||
let doc = match &option.documentation {
|
let doc = match &option.documentation {
|
||||||
Some(lsp::Documentation::String(contents))
|
Some(lsp::Documentation::String(contents))
|
||||||
@ -246,42 +249,60 @@ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
value: contents,
|
value: contents,
|
||||||
})) => {
|
})) => {
|
||||||
// TODO: convert to wrapped text
|
// TODO: convert to wrapped text
|
||||||
Markdown::new(format!(
|
Markdown::new(
|
||||||
"```{}\n{}\n```\n{}",
|
format!(
|
||||||
language,
|
"```{}\n{}\n```\n{}",
|
||||||
option.detail.as_deref().unwrap_or_default(),
|
language,
|
||||||
contents.clone()
|
option.detail.as_deref().unwrap_or_default(),
|
||||||
))
|
contents.clone()
|
||||||
|
),
|
||||||
|
cx.editor.syn_loader.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||||
kind: lsp::MarkupKind::Markdown,
|
kind: lsp::MarkupKind::Markdown,
|
||||||
value: contents,
|
value: contents,
|
||||||
})) => {
|
})) => {
|
||||||
// TODO: set language based on doc scope
|
// TODO: set language based on doc scope
|
||||||
Markdown::new(format!(
|
Markdown::new(
|
||||||
"```{}\n{}\n```\n{}",
|
format!(
|
||||||
language,
|
"```{}\n{}\n```\n{}",
|
||||||
option.detail.as_deref().unwrap_or_default(),
|
language,
|
||||||
contents.clone()
|
option.detail.as_deref().unwrap_or_default(),
|
||||||
))
|
contents.clone()
|
||||||
|
),
|
||||||
|
cx.editor.syn_loader.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
None if option.detail.is_some() => {
|
None if option.detail.is_some() => {
|
||||||
// TODO: copied from above
|
// TODO: copied from above
|
||||||
|
|
||||||
// TODO: set language based on doc scope
|
// TODO: set language based on doc scope
|
||||||
Markdown::new(format!(
|
Markdown::new(
|
||||||
"```{}\n{}\n```",
|
format!(
|
||||||
language,
|
"```{}\n{}\n```",
|
||||||
option.detail.as_deref().unwrap_or_default(),
|
language,
|
||||||
))
|
option.detail.as_deref().unwrap_or_default(),
|
||||||
|
),
|
||||||
|
cx.editor.syn_loader.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let half = area.height / 2;
|
let half = area.height / 2;
|
||||||
let height = 15.min(half);
|
let height = 15.min(half);
|
||||||
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
// we want to make sure the cursor is visible (not hidden behind the documentation)
|
||||||
let area = Rect::new(0, area.height - height - 2, area.width, height);
|
let y = if cursor_pos + view.area.y
|
||||||
|
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
|
||||||
|
{
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
||||||
|
area.height.saturating_sub(height).saturating_sub(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
let area = Rect::new(0, y, area.width, height);
|
||||||
|
|
||||||
// clear area
|
// clear area
|
||||||
let background = cx.editor.theme.get("ui.popup");
|
let background = cx.editor.theme.get("ui.popup");
|
||||||
|
@ -11,13 +11,12 @@
|
|||||||
syntax::{self, HighlightEvent},
|
syntax::{self, HighlightEvent},
|
||||||
LineEnding, Position, Range,
|
LineEnding, Position, Range,
|
||||||
};
|
};
|
||||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
|
||||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor,
|
cursor,
|
||||||
event::{read, Event, EventStream},
|
event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
||||||
};
|
};
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
@ -130,7 +129,7 @@ pub fn render_buffer(
|
|||||||
})],
|
})],
|
||||||
};
|
};
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
let mut visual_x = 0;
|
let mut visual_x = 0u16;
|
||||||
let mut line = 0u16;
|
let mut line = 0u16;
|
||||||
let tab_width = doc.tab_width();
|
let tab_width = doc.tab_width();
|
||||||
|
|
||||||
@ -186,7 +185,7 @@ pub fn render_buffer(
|
|||||||
break 'outer;
|
break 'outer;
|
||||||
}
|
}
|
||||||
} else if grapheme == "\t" {
|
} else if grapheme == "\t" {
|
||||||
visual_x += (tab_width as u16);
|
visual_x = visual_x.saturating_add(tab_width as u16);
|
||||||
} else {
|
} else {
|
||||||
let out_of_bounds = visual_x < view.first_col as u16
|
let out_of_bounds = visual_x < view.first_col as u16
|
||||||
|| visual_x >= viewport.width + view.first_col as u16;
|
|| visual_x >= viewport.width + view.first_col as u16;
|
||||||
@ -198,7 +197,7 @@ pub fn render_buffer(
|
|||||||
|
|
||||||
if out_of_bounds {
|
if out_of_bounds {
|
||||||
// if we're offscreen just keep going until we hit a new line
|
// if we're offscreen just keep going until we hit a new line
|
||||||
visual_x += width;
|
visual_x = visual_x.saturating_add(width);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,8 +607,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
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) => {
|
||||||
let mut key = KeyEvent::from(key);
|
|
||||||
canonicalize_key(&mut key);
|
canonicalize_key(&mut key);
|
||||||
// clear status
|
// clear status
|
||||||
cx.editor.status_msg = None;
|
cx.editor.status_msg = None;
|
||||||
|
@ -7,25 +7,34 @@
|
|||||||
text::Text,
|
text::Text,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use helix_core::Position;
|
use helix_core::{syntax, Position};
|
||||||
use helix_view::{Editor, Theme};
|
use helix_view::{Editor, Theme};
|
||||||
|
|
||||||
pub struct Markdown {
|
pub struct Markdown {
|
||||||
contents: String,
|
contents: String,
|
||||||
|
|
||||||
|
config_loader: Arc<syntax::Loader>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: pre-render and self reference via Pin
|
// TODO: pre-render and self reference via Pin
|
||||||
// better yet, just use Tendril + subtendril for references
|
// better yet, just use Tendril + subtendril for references
|
||||||
|
|
||||||
impl Markdown {
|
impl Markdown {
|
||||||
pub fn new(contents: String) -> Self {
|
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||||
Self { contents }
|
Self {
|
||||||
|
contents,
|
||||||
|
config_loader,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
fn parse<'a>(
|
||||||
|
contents: &'a str,
|
||||||
|
theme: Option<&Theme>,
|
||||||
|
loader: &syntax::Loader,
|
||||||
|
) -> tui::text::Text<'a> {
|
||||||
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||||
use tui::text::{Span, Spans, Text};
|
use tui::text::{Span, Spans, Text};
|
||||||
|
|
||||||
@ -79,9 +88,7 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
|
|||||||
use helix_core::Rope;
|
use helix_core::Rope;
|
||||||
|
|
||||||
let rope = Rope::from(text.as_ref());
|
let rope = Rope::from(text.as_ref());
|
||||||
let syntax = syntax::LOADER
|
let syntax = loader
|
||||||
.get()
|
|
||||||
.unwrap()
|
|
||||||
.language_config_for_scope(&format!("source.{}", language))
|
.language_config_for_scope(&format!("source.{}", language))
|
||||||
.and_then(|config| config.highlight_config(theme.scopes()))
|
.and_then(|config| config.highlight_config(theme.scopes()))
|
||||||
.map(|config| Syntax::new(&rope, config));
|
.map(|config| Syntax::new(&rope, config));
|
||||||
@ -101,9 +108,7 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
|
|||||||
}
|
}
|
||||||
HighlightEvent::Source { start, end } => {
|
HighlightEvent::Source { start, end } => {
|
||||||
let style = match highlights.first() {
|
let style = match highlights.first() {
|
||||||
Some(span) => {
|
Some(span) => theme.get(&theme.scopes()[span.0]),
|
||||||
theme.get(theme.scopes()[span.0].as_str())
|
|
||||||
}
|
|
||||||
None => text_style,
|
None => text_style,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,7 +164,6 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Code(text) | Event::Html(text) => {
|
Event::Code(text) | Event::Html(text) => {
|
||||||
log::warn!("code {:?}", text);
|
|
||||||
let mut span = to_span(text);
|
let mut span = to_span(text);
|
||||||
span.style = code_style;
|
span.style = code_style;
|
||||||
spans.push(span);
|
spans.push(span);
|
||||||
@ -198,7 +202,7 @@ impl Component for Markdown {
|
|||||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||||
use tui::widgets::{Paragraph, Widget, Wrap};
|
use tui::widgets::{Paragraph, Widget, Wrap};
|
||||||
|
|
||||||
let text = parse(&self.contents, Some(&cx.editor.theme));
|
let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
|
||||||
|
|
||||||
let par = Paragraph::new(text)
|
let par = Paragraph::new(text)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
@ -209,7 +213,7 @@ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||||
let contents = parse(&self.contents, None);
|
let contents = parse(&self.contents, None, &self.config_loader);
|
||||||
let padding = 2;
|
let padding = 2;
|
||||||
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
|
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
|
||||||
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
|
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
|
||||||
|
@ -115,10 +115,43 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
|||||||
|
|
||||||
pub mod completers {
|
pub mod completers {
|
||||||
use crate::ui::prompt::Completion;
|
use crate::ui::prompt::Completion;
|
||||||
use std::borrow::Cow;
|
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||||
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
|
use helix_view::theme;
|
||||||
|
use std::cmp::Reverse;
|
||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
pub type Completer = fn(&str) -> Vec<Completion>;
|
pub type Completer = fn(&str) -> Vec<Completion>;
|
||||||
|
|
||||||
|
pub fn theme(input: &str) -> Vec<Completion> {
|
||||||
|
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
|
||||||
|
names.extend(theme::Loader::read_names(
|
||||||
|
&helix_core::config_dir().join("themes"),
|
||||||
|
));
|
||||||
|
names.push("default".into());
|
||||||
|
|
||||||
|
let mut names: Vec<_> = names
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| ((0..), Cow::from(name)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let matcher = Matcher::default();
|
||||||
|
|
||||||
|
let mut matches: Vec<_> = names
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(range, name)| {
|
||||||
|
matcher
|
||||||
|
.fuzzy_match(&name, &input)
|
||||||
|
.map(|score| (name, score))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
|
||||||
|
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
|
||||||
|
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
|
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
|
||||||
pub fn filename(input: &str) -> Vec<Completion> {
|
pub fn filename(input: &str) -> Vec<Completion> {
|
||||||
// Rust's filename handling is really annoying.
|
// Rust's filename handling is really annoying.
|
||||||
@ -178,10 +211,6 @@ pub fn filename(input: &str) -> Vec<Completion> {
|
|||||||
|
|
||||||
// if empty, return a list of dirs and files in current dir
|
// if empty, return a list of dirs and files in current dir
|
||||||
if let Some(file_name) = file_name {
|
if let Some(file_name) = file_name {
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
|
||||||
use std::cmp::Reverse;
|
|
||||||
|
|
||||||
let matcher = Matcher::default();
|
let matcher = Matcher::default();
|
||||||
|
|
||||||
// inefficient, but we need to calculate the scores, filter out None, then sort.
|
// inefficient, but we need to calculate the scores, filter out None, then sort.
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
use std::{borrow::Cow, ops::RangeFrom};
|
use std::{borrow::Cow, ops::RangeFrom};
|
||||||
use tui::terminal::CursorKind;
|
use tui::terminal::CursorKind;
|
||||||
|
|
||||||
|
use helix_core::{
|
||||||
|
unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
|
||||||
|
unicode::width::UnicodeWidthStr,
|
||||||
|
};
|
||||||
|
|
||||||
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
||||||
|
|
||||||
pub struct Prompt {
|
pub struct Prompt {
|
||||||
@ -34,6 +39,17 @@ pub enum CompletionDirection {
|
|||||||
Backward,
|
Backward,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Movement {
|
||||||
|
BackwardChar(usize),
|
||||||
|
BackwardWord(usize),
|
||||||
|
ForwardChar(usize),
|
||||||
|
ForwardWord(usize),
|
||||||
|
StartOfLine,
|
||||||
|
EndOfLine,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
prompt: String,
|
prompt: String,
|
||||||
@ -52,30 +68,120 @@ pub fn new(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute the cursor position after applying movement
|
||||||
|
/// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
|
||||||
|
fn eval_movement(&self, movement: Movement) -> usize {
|
||||||
|
match movement {
|
||||||
|
Movement::BackwardChar(rep) => {
|
||||||
|
let mut position = self.cursor;
|
||||||
|
for _ in 0..rep {
|
||||||
|
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
|
||||||
|
if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
|
||||||
|
position = pos;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position
|
||||||
|
}
|
||||||
|
Movement::BackwardWord(rep) => {
|
||||||
|
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
|
||||||
|
if char_indices.is_empty() {
|
||||||
|
return self.cursor;
|
||||||
|
}
|
||||||
|
let mut char_position = char_indices
|
||||||
|
.iter()
|
||||||
|
.position(|(idx, _)| *idx == self.cursor)
|
||||||
|
.unwrap_or(char_indices.len() - 1);
|
||||||
|
|
||||||
|
for _ in 0..rep {
|
||||||
|
if char_position == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut found = None;
|
||||||
|
for prev in (0..char_position - 1).rev() {
|
||||||
|
if char_indices[prev].1.is_whitespace() {
|
||||||
|
found = Some(prev + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char_position = found.unwrap_or(0);
|
||||||
|
}
|
||||||
|
char_indices[char_position].0
|
||||||
|
}
|
||||||
|
Movement::ForwardWord(rep) => {
|
||||||
|
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
|
||||||
|
if char_indices.is_empty() {
|
||||||
|
return self.cursor;
|
||||||
|
}
|
||||||
|
let mut char_position = char_indices
|
||||||
|
.iter()
|
||||||
|
.position(|(idx, _)| *idx == self.cursor)
|
||||||
|
.unwrap_or_else(|| char_indices.len());
|
||||||
|
|
||||||
|
for _ in 0..rep {
|
||||||
|
// Skip any non-whitespace characters
|
||||||
|
while char_position < char_indices.len()
|
||||||
|
&& !char_indices[char_position].1.is_whitespace()
|
||||||
|
{
|
||||||
|
char_position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip any whitespace characters
|
||||||
|
while char_position < char_indices.len()
|
||||||
|
&& char_indices[char_position].1.is_whitespace()
|
||||||
|
{
|
||||||
|
char_position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are now on the start of the next word
|
||||||
|
}
|
||||||
|
char_indices
|
||||||
|
.get(char_position)
|
||||||
|
.map(|(i, _)| *i)
|
||||||
|
.unwrap_or_else(|| self.line.len())
|
||||||
|
}
|
||||||
|
Movement::ForwardChar(rep) => {
|
||||||
|
let mut position = self.cursor;
|
||||||
|
for _ in 0..rep {
|
||||||
|
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
|
||||||
|
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||||
|
position = pos;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position
|
||||||
|
}
|
||||||
|
Movement::StartOfLine => 0,
|
||||||
|
Movement::EndOfLine => {
|
||||||
|
let mut cursor =
|
||||||
|
GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
|
||||||
|
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||||
|
pos
|
||||||
|
} else {
|
||||||
|
self.cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Movement::None => self.cursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_char(&mut self, c: char) {
|
pub fn insert_char(&mut self, c: char) {
|
||||||
let pos = if self.line.is_empty() {
|
self.line.insert(self.cursor, c);
|
||||||
0
|
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
|
||||||
} else {
|
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||||
self.line
|
self.cursor = pos;
|
||||||
.char_indices()
|
}
|
||||||
.nth(self.cursor)
|
|
||||||
.map(|(pos, _)| pos)
|
|
||||||
.unwrap_or_else(|| self.line.len())
|
|
||||||
};
|
|
||||||
self.line.insert(pos, c);
|
|
||||||
self.cursor += 1;
|
|
||||||
self.completion = (self.completion_fn)(&self.line);
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_char_left(&mut self) {
|
pub fn move_cursor(&mut self, movement: Movement) {
|
||||||
self.cursor = self.cursor.saturating_sub(1)
|
let pos = self.eval_movement(movement);
|
||||||
}
|
self.cursor = pos
|
||||||
|
|
||||||
pub fn move_char_right(&mut self) {
|
|
||||||
if self.cursor < self.line.len() {
|
|
||||||
self.cursor += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_start(&mut self) {
|
pub fn move_start(&mut self) {
|
||||||
@ -87,39 +193,29 @@ pub fn move_end(&mut self) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_char_backwards(&mut self) {
|
pub fn delete_char_backwards(&mut self) {
|
||||||
if self.cursor > 0 {
|
let pos = self.eval_movement(Movement::BackwardChar(1));
|
||||||
let pos = self
|
self.line.replace_range(pos..self.cursor, "");
|
||||||
.line
|
self.cursor = pos;
|
||||||
.char_indices()
|
|
||||||
.nth(self.cursor - 1)
|
|
||||||
.map(|(pos, _)| pos)
|
|
||||||
.expect("line is not empty");
|
|
||||||
self.line.remove(pos);
|
|
||||||
self.cursor -= 1;
|
|
||||||
self.completion = (self.completion_fn)(&self.line);
|
|
||||||
}
|
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_word_backwards(&mut self) {
|
pub fn delete_word_backwards(&mut self) {
|
||||||
use helix_core::get_general_category;
|
let pos = self.eval_movement(Movement::BackwardWord(1));
|
||||||
let mut chars = self.line.char_indices().rev();
|
self.line.replace_range(pos..self.cursor, "");
|
||||||
// TODO add skipping whitespace logic here
|
self.cursor = pos;
|
||||||
let (mut i, cat) = match chars.next() {
|
|
||||||
Some((i, c)) => (i, get_general_category(c)),
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
self.cursor -= 1;
|
|
||||||
for (nn, nc) in chars {
|
|
||||||
if get_general_category(nc) != cat {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i = nn;
|
|
||||||
self.cursor -= 1;
|
|
||||||
}
|
|
||||||
self.line.drain(i..);
|
|
||||||
self.completion = (self.completion_fn)(&self.line);
|
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kill_to_end_of_line(&mut self) {
|
||||||
|
let pos = self.eval_movement(Movement::EndOfLine);
|
||||||
|
self.line.replace_range(self.cursor..pos, "");
|
||||||
|
|
||||||
|
self.exit_selection();
|
||||||
|
self.completion = (self.completion_fn)(&self.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
@ -293,31 +389,71 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
||||||
}
|
}
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
code: KeyCode::Esc, ..
|
code: KeyCode::Esc, ..
|
||||||
} => {
|
} => {
|
||||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
|
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
|
||||||
return close_fn;
|
return close_fn;
|
||||||
}
|
}
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('f'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
code: KeyCode::Right,
|
code: KeyCode::Right,
|
||||||
..
|
..
|
||||||
} => self.move_char_right(),
|
} => self.move_cursor(Movement::ForwardChar(1)),
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('b'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
code: KeyCode::Left,
|
code: KeyCode::Left,
|
||||||
..
|
..
|
||||||
} => self.move_char_left(),
|
} => self.move_cursor(Movement::BackwardChar(1)),
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
|
code: KeyCode::End,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
code: KeyCode::Char('e'),
|
code: KeyCode::Char('e'),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
} => self.move_end(),
|
} => self.move_end(),
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
|
code: KeyCode::Home,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
code: KeyCode::Char('a'),
|
code: KeyCode::Char('a'),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
} => self.move_start(),
|
} => self.move_start(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
modifiers: KeyModifiers::ALT,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
|
code: KeyCode::Char('b'),
|
||||||
|
modifiers: KeyModifiers::ALT,
|
||||||
|
} => self.move_cursor(Movement::BackwardWord(1)),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Right,
|
||||||
|
modifiers: KeyModifiers::ALT,
|
||||||
|
}
|
||||||
|
| KeyEvent {
|
||||||
|
code: KeyCode::Char('f'),
|
||||||
|
modifiers: KeyModifiers::ALT,
|
||||||
|
} => self.move_cursor(Movement::ForwardWord(1)),
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char('w'),
|
code: KeyCode::Char('w'),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
} => self.delete_word_backwards(),
|
} => self.delete_word_backwards(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('k'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
} => self.kill_to_end_of_line(),
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Backspace,
|
code: KeyCode::Backspace,
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
@ -363,7 +499,9 @@ fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind)
|
|||||||
(
|
(
|
||||||
Some(Position::new(
|
Some(Position::new(
|
||||||
area.y as usize + line,
|
area.y as usize + line,
|
||||||
area.x as usize + self.prompt.len() + self.cursor,
|
area.x as usize
|
||||||
|
+ self.prompt.len()
|
||||||
|
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
|
||||||
)),
|
)),
|
||||||
CursorKind::Block,
|
CursorKind::Block,
|
||||||
)
|
)
|
||||||
|
@ -203,16 +203,6 @@ pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||||
///
|
|
||||||
/// ```should_panic
|
|
||||||
/// # use helix_tui::buffer::Buffer;
|
|
||||||
/// # use helix_tui::layout::Rect;
|
|
||||||
/// let rect = Rect::new(200, 100, 10, 10);
|
|
||||||
/// let buffer = Buffer::empty(rect);
|
|
||||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
|
||||||
/// // starts at (200, 100).
|
|
||||||
/// buffer.index_of(0, 0); // Panics
|
|
||||||
/// ```
|
|
||||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
x >= self.area.left()
|
x >= self.area.left()
|
||||||
@ -245,15 +235,6 @@ pub fn index_of(&self, x: u16, y: u16) -> usize {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics when given an index that is outside the Buffer's content.
|
/// Panics when given an index that is outside the Buffer's content.
|
||||||
///
|
|
||||||
/// ```should_panic
|
|
||||||
/// # use helix_tui::buffer::Buffer;
|
|
||||||
/// # use helix_tui::layout::Rect;
|
|
||||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
|
||||||
/// let buffer = Buffer::empty(rect);
|
|
||||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
|
||||||
/// buffer.pos_of(100); // Panics
|
|
||||||
/// ```
|
|
||||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
i < self.content.len(),
|
i < self.content.len(),
|
||||||
@ -510,6 +491,7 @@ fn it_translates_to_and_from_coordinates() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "outside the buffer")]
|
#[should_panic(expected = "outside the buffer")]
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
fn pos_of_panics_on_out_of_bounds() {
|
fn pos_of_panics_on_out_of_bounds() {
|
||||||
let rect = Rect::new(0, 0, 10, 10);
|
let rect = Rect::new(0, 0, 10, 10);
|
||||||
let buf = Buffer::empty(rect);
|
let buf = Buffer::empty(rect);
|
||||||
@ -520,6 +502,7 @@ fn pos_of_panics_on_out_of_bounds() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "outside the buffer")]
|
#[should_panic(expected = "outside the buffer")]
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
fn index_of_panics_on_out_of_bounds() {
|
fn index_of_panics_on_out_of_bounds() {
|
||||||
let rect = Rect::new(0, 0, 10, 10);
|
let rect = Rect::new(0, 0, 10, 10);
|
||||||
let buf = Buffer::empty(rect);
|
let buf = Buffer::empty(rect);
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
//! implement your own.
|
//! implement your own.
|
||||||
//!
|
//!
|
||||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||||
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
|
//! to customize them. The widget is then rendered using the `Frame::render_widget` which take
|
||||||
//! your widget instance an area to draw to.
|
//! your widget instance an area to draw to.
|
||||||
//!
|
//!
|
||||||
//! The following example renders a block of the size of the terminal:
|
//! The following example renders a block of the size of the terminal:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
//! `widgets` is a collection of types that implement [`Widget`].
|
||||||
//!
|
//!
|
||||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
||||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
||||||
|
@ -34,3 +34,6 @@ slotmap = "1"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
log = "~0.4"
|
log = "~0.4"
|
||||||
|
|
||||||
|
which = "4.1"
|
||||||
|
|
||||||
|
193
helix-view/src/clipboard.rs
Normal file
193
helix-view/src/clipboard.rs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub trait ClipboardProvider: std::fmt::Debug {
|
||||||
|
fn name(&self) -> Cow<str>;
|
||||||
|
fn get_contents(&self) -> Result<String>;
|
||||||
|
fn set_contents(&self, contents: String) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! command_provider {
|
||||||
|
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
|
||||||
|
Box::new(provider::CommandProvider {
|
||||||
|
get_cmd: provider::CommandConfig {
|
||||||
|
prg: $get_prg,
|
||||||
|
args: &[ $( $get_arg ),* ],
|
||||||
|
},
|
||||||
|
set_cmd: provider::CommandConfig {
|
||||||
|
prg: $set_prg,
|
||||||
|
args: &[ $( $set_arg ),* ],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||||
|
// TODO: support for user-defined provider, probably when we have plugin support by setting a
|
||||||
|
// variable?
|
||||||
|
|
||||||
|
if exists("pbcopy") && exists("pbpaste") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "pbpaste";
|
||||||
|
copy => "pbcopy";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "wl-paste", "--no-newline";
|
||||||
|
copy => "wl-copy", "--foreground", "--type", "text/plain";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("DISPLAY") && exists("xclip") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "xclip", "-o", "-selection", "clipboard";
|
||||||
|
copy => "xclip", "-i", "-selection", "clipboard";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
|
||||||
|
{
|
||||||
|
// FIXME: check performance of is_exit_success
|
||||||
|
command_provider! {
|
||||||
|
paste => "xsel", "-o", "-b";
|
||||||
|
copy => "xsel", "--nodetach", "-i", "-b";
|
||||||
|
}
|
||||||
|
} else if exists("lemonade") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "lemonade", "paste";
|
||||||
|
copy => "lemonade", "copy";
|
||||||
|
}
|
||||||
|
} else if exists("doitclient") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "doitclient", "wclip", "-r";
|
||||||
|
copy => "doitclient", "wclip";
|
||||||
|
}
|
||||||
|
} else if exists("win32yank.exe") {
|
||||||
|
// FIXME: does it work within WSL?
|
||||||
|
command_provider! {
|
||||||
|
paste => "win32yank.exe", "-o", "--lf";
|
||||||
|
copy => "win32yank.exe", "-i", "--crlf";
|
||||||
|
}
|
||||||
|
} else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "termux-clipboard-get";
|
||||||
|
copy => "termux-clipboard-set";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("TMUX") && exists("tmux") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "tmux", "save-buffer", "-";
|
||||||
|
copy => "tmux", "load-buffer", "-";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box::new(provider::NopProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(executable_name: &str) -> bool {
|
||||||
|
which::which(executable_name).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_var_is_set(env_var_name: &str) -> bool {
|
||||||
|
std::env::var_os(env_var_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_exit_success(program: &str, args: &[&str]) -> bool {
|
||||||
|
std::process::Command::new(program)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
mod provider {
|
||||||
|
use super::ClipboardProvider;
|
||||||
|
use anyhow::{bail, Context as _, Result};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NopProvider;
|
||||||
|
|
||||||
|
impl ClipboardProvider for NopProvider {
|
||||||
|
fn name(&self) -> Cow<str> {
|
||||||
|
Cow::Borrowed("none")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_contents(&self) -> Result<String> {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_contents(&self, _: String) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandConfig {
|
||||||
|
pub prg: &'static str,
|
||||||
|
pub args: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandConfig {
|
||||||
|
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
|
||||||
|
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
|
||||||
|
|
||||||
|
let mut child = Command::new(self.prg)
|
||||||
|
.args(self.args)
|
||||||
|
.stdin(stdin)
|
||||||
|
.stdout(stdout)
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
if let Some(input) = input {
|
||||||
|
let mut stdin = child.stdin.take().context("stdin is missing")?;
|
||||||
|
stdin
|
||||||
|
.write_all(input.as_bytes())
|
||||||
|
.context("couldn't write in stdin")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add timer?
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!("clipboard provider {} failed", self.prg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if pipe_output {
|
||||||
|
Ok(Some(String::from_utf8(output.stdout)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandProvider {
|
||||||
|
pub get_cmd: CommandConfig,
|
||||||
|
pub set_cmd: CommandConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipboardProvider for CommandProvider {
|
||||||
|
fn name(&self) -> Cow<str> {
|
||||||
|
if self.get_cmd.prg != self.set_cmd.prg {
|
||||||
|
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(self.get_cmd.prg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_contents(&self) -> Result<String> {
|
||||||
|
let output = self
|
||||||
|
.get_cmd
|
||||||
|
.execute(None, true)?
|
||||||
|
.context("output is missing")?;
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_contents(&self, value: String) -> Result<()> {
|
||||||
|
self.set_cmd.execute(Some(&value), false).map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
use anyhow::{anyhow, Context, Error};
|
use anyhow::{anyhow, Context, Error};
|
||||||
use serde::de::{self, Deserialize, Deserializer};
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
@ -12,12 +10,14 @@
|
|||||||
auto_detect_line_ending,
|
auto_detect_line_ending,
|
||||||
chars::{char_is_line_ending, char_is_whitespace},
|
chars::{char_is_line_ending, char_is_whitespace},
|
||||||
history::History,
|
history::History,
|
||||||
syntax::{LanguageConfiguration, LOADER},
|
syntax::{self, LanguageConfiguration},
|
||||||
ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
|
ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
|
||||||
DEFAULT_LINE_ENDING,
|
DEFAULT_LINE_ENDING,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{DocumentId, ViewId};
|
use crate::{DocumentId, Theme, ViewId};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@ -26,40 +26,6 @@ pub enum Mode {
|
|||||||
Insert,
|
Insert,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Mode {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Mode::Normal => f.write_str("normal"),
|
|
||||||
Mode::Select => f.write_str("select"),
|
|
||||||
Mode::Insert => f.write_str("insert"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Mode {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"normal" => Ok(Mode::Normal),
|
|
||||||
"select" => Ok(Mode::Select),
|
|
||||||
"insert" => Ok(Mode::Insert),
|
|
||||||
_ => Err(anyhow!("Invalid mode '{}'", s)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toml deserializer doesn't seem to recognize string as enum
|
|
||||||
impl<'de> Deserialize<'de> for Mode {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
s.parse().map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum IndentStyle {
|
pub enum IndentStyle {
|
||||||
Tabs,
|
Tabs,
|
||||||
@ -127,6 +93,29 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Mode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Mode::Normal => f.write_str("normal"),
|
||||||
|
Mode::Select => f.write_str("select"),
|
||||||
|
Mode::Insert => f.write_str("insert"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Mode {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"normal" => Ok(Mode::Normal),
|
||||||
|
"select" => Ok(Mode::Select),
|
||||||
|
"insert" => Ok(Mode::Insert),
|
||||||
|
_ => Err(anyhow!("Invalid mode '{}'", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Like std::mem::replace() except it allows the replacement value to be mapped from the
|
/// Like std::mem::replace() except it allows the replacement value to be mapped from the
|
||||||
/// original value.
|
/// original value.
|
||||||
fn take_with<T, F>(mut_ref: &mut T, closure: F)
|
fn take_with<T, F>(mut_ref: &mut T, closure: F)
|
||||||
@ -181,7 +170,7 @@ pub fn fold_home_dir(path: &Path) -> PathBuf {
|
|||||||
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
|
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
|
||||||
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
||||||
/// needs to improve on.
|
/// needs to improve on.
|
||||||
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
|
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
|
||||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||||
let path = expand_tilde(path);
|
let path = expand_tilde(path);
|
||||||
let mut components = path.components().peekable();
|
let mut components = path.components().peekable();
|
||||||
@ -253,7 +242,11 @@ pub fn new(text: Rope) -> Self {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: async fn?
|
// TODO: async fn?
|
||||||
pub fn load(path: PathBuf) -> Result<Self, Error> {
|
pub fn load(
|
||||||
|
path: PathBuf,
|
||||||
|
theme: Option<&Theme>,
|
||||||
|
config_loader: Option<&syntax::Loader>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
use std::{fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader};
|
||||||
|
|
||||||
let mut doc = if !path.exists() {
|
let mut doc = if !path.exists() {
|
||||||
@ -277,6 +270,10 @@ pub fn load(path: PathBuf) -> Result<Self, Error> {
|
|||||||
doc.detect_indent_style();
|
doc.detect_indent_style();
|
||||||
doc.set_line_ending(line_ending);
|
doc.set_line_ending(line_ending);
|
||||||
|
|
||||||
|
if let Some(loader) = config_loader {
|
||||||
|
doc.detect_language(theme, loader);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(doc)
|
Ok(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,12 +348,10 @@ pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_language(&mut self) {
|
pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
|
||||||
if let Some(path) = self.path() {
|
if let Some(path) = &self.path {
|
||||||
let loader = LOADER.get().unwrap();
|
let language_config = config_loader.language_config_for_file_name(path);
|
||||||
let language_config = loader.language_config_for_file_name(path);
|
self.set_language(theme, language_config);
|
||||||
let scopes = loader.scopes();
|
|
||||||
self.set_language(language_config, scopes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,18 +488,16 @@ pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
|
|||||||
// and error out when document is saved
|
// and error out when document is saved
|
||||||
self.path = Some(path);
|
self.path = Some(path);
|
||||||
|
|
||||||
// try detecting the language based on filepath
|
|
||||||
self.detect_language();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_language(
|
pub fn set_language(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
theme: Option<&Theme>,
|
||||||
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
|
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
|
||||||
scopes: &[String],
|
|
||||||
) {
|
) {
|
||||||
if let Some(language_config) = language_config {
|
if let Some(language_config) = language_config {
|
||||||
|
let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
|
||||||
if let Some(highlight_config) = language_config.highlight_config(scopes) {
|
if let Some(highlight_config) = language_config.highlight_config(scopes) {
|
||||||
let syntax = Syntax::new(&self.text, highlight_config);
|
let syntax = Syntax::new(&self.text, highlight_config);
|
||||||
self.syntax = Some(syntax);
|
self.syntax = Some(syntax);
|
||||||
@ -518,12 +511,15 @@ pub fn set_language(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_language2(&mut self, scope: &str) {
|
pub fn set_language2(
|
||||||
let loader = LOADER.get().unwrap();
|
&mut self,
|
||||||
let language_config = loader.language_config_for_scope(scope);
|
scope: &str,
|
||||||
let scopes = loader.scopes();
|
theme: Option<&Theme>,
|
||||||
|
config_loader: Arc<syntax::Loader>,
|
||||||
|
) {
|
||||||
|
let language_config = config_loader.language_config_for_scope(scope);
|
||||||
|
|
||||||
self.set_language(language_config, scopes);
|
self.set_language(theme, language_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
|
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
|
use crate::clipboard::{get_clipboard_provider, ClipboardProvider};
|
||||||
|
use crate::{
|
||||||
|
theme::{self, Theme},
|
||||||
|
tree::Tree,
|
||||||
|
Document, DocumentId, RegisterSelection, View, ViewId,
|
||||||
|
};
|
||||||
|
use helix_core::syntax;
|
||||||
use tui::layout::Rect;
|
use tui::layout::Rect;
|
||||||
use tui::terminal::CursorKind;
|
use tui::terminal::CursorKind;
|
||||||
|
|
||||||
use futures_util::future;
|
use futures_util::future;
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use slotmap::SlotMap;
|
use slotmap::SlotMap;
|
||||||
|
|
||||||
@ -23,6 +28,10 @@ pub struct Editor {
|
|||||||
pub registers: Registers,
|
pub registers: Registers,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub language_servers: helix_lsp::Registry,
|
pub language_servers: helix_lsp::Registry,
|
||||||
|
pub clipboard_provider: Box<dyn ClipboardProvider>,
|
||||||
|
|
||||||
|
pub syn_loader: Arc<syntax::Loader>,
|
||||||
|
pub theme_loader: Arc<theme::Loader>,
|
||||||
|
|
||||||
pub status_msg: Option<(String, Severity)>,
|
pub status_msg: Option<(String, Severity)>,
|
||||||
}
|
}
|
||||||
@ -35,27 +44,11 @@ pub enum Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub fn new(mut area: tui::layout::Rect) -> Self {
|
pub fn new(
|
||||||
use helix_core::config_dir;
|
mut area: tui::layout::Rect,
|
||||||
let config = std::fs::read(config_dir().join("theme.toml"));
|
themes: Arc<theme::Loader>,
|
||||||
// load $HOME/.config/helix/theme.toml, fallback to default config
|
config_loader: Arc<syntax::Loader>,
|
||||||
let toml = config
|
) -> Self {
|
||||||
.as_deref()
|
|
||||||
.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();
|
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
|
||||||
@ -66,9 +59,12 @@ pub fn new(mut area: tui::layout::Rect) -> Self {
|
|||||||
documents: SlotMap::with_key(),
|
documents: SlotMap::with_key(),
|
||||||
count: None,
|
count: None,
|
||||||
selected_register: RegisterSelection::default(),
|
selected_register: RegisterSelection::default(),
|
||||||
theme,
|
theme: themes.default(),
|
||||||
language_servers,
|
language_servers,
|
||||||
|
syn_loader: config_loader,
|
||||||
|
theme_loader: themes,
|
||||||
registers: Registers::default(),
|
registers: Registers::default(),
|
||||||
|
clipboard_provider: get_clipboard_provider(),
|
||||||
status_msg: None,
|
status_msg: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,6 +81,32 @@ pub fn set_error(&mut self, error: String) {
|
|||||||
self.status_msg = Some((error, Severity::Error));
|
self.status_msg = Some((error, Severity::Error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_theme(&mut self, theme: Theme) {
|
||||||
|
let scopes = theme.scopes();
|
||||||
|
for config in self
|
||||||
|
.syn_loader
|
||||||
|
.language_configs_iter()
|
||||||
|
.filter(|cfg| cfg.is_highlight_initialized())
|
||||||
|
{
|
||||||
|
config.reconfigure(scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.theme = theme;
|
||||||
|
self._refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_theme_from_name(&mut self, theme: &str) {
|
||||||
|
let theme = match self.theme_loader.load(theme.as_ref()) {
|
||||||
|
Ok(theme) => theme,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed setting theme `{}` - {}", theme, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.set_theme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
fn _refresh(&mut self) {
|
fn _refresh(&mut self) {
|
||||||
for (view, _) in self.tree.views_mut() {
|
for (view, _) in self.tree.views_mut() {
|
||||||
let doc = &self.documents[view.doc];
|
let doc = &self.documents[view.doc];
|
||||||
@ -168,7 +190,7 @@ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Erro
|
|||||||
let id = if let Some(id) = id {
|
let id = if let Some(id) = id {
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
let mut doc = Document::load(path)?;
|
let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?;
|
||||||
|
|
||||||
// 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
|
||||||
@ -254,6 +276,10 @@ pub fn documents(&self) -> impl Iterator<Item = &Document> {
|
|||||||
self.documents.iter().map(|(_id, doc)| doc)
|
self.documents.iter().map(|(_id, doc)| doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
|
||||||
|
self.documents.iter_mut().map(|(_id, doc)| doc)
|
||||||
|
}
|
||||||
|
|
||||||
// pub fn current_document(&self) -> Document {
|
// pub fn current_document(&self) -> Document {
|
||||||
// let id = self.view().doc;
|
// let id = self.view().doc;
|
||||||
// let doc = &mut editor.documents[id];
|
// let doc = &mut editor.documents[id];
|
||||||
|
@ -1,226 +0,0 @@
|
|||||||
//! Input event handling, currently backed by crossterm.
|
|
||||||
use anyhow::{anyhow, Error};
|
|
||||||
use crossterm::event;
|
|
||||||
use serde::de::{self, Deserialize, Deserializer};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
pub use crossterm::event::{KeyCode, KeyModifiers};
|
|
||||||
|
|
||||||
/// Represents a key event.
|
|
||||||
// We use a newtype here because we want to customize Deserialize and Display.
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
|
|
||||||
pub struct KeyEvent {
|
|
||||||
pub code: KeyCode,
|
|
||||||
pub modifiers: KeyModifiers,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for KeyEvent {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_fmt(format_args!(
|
|
||||||
"{}{}{}",
|
|
||||||
if self.modifiers.contains(KeyModifiers::SHIFT) {
|
|
||||||
"S-"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
if self.modifiers.contains(KeyModifiers::ALT) {
|
|
||||||
"A-"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
if self.modifiers.contains(KeyModifiers::CONTROL) {
|
|
||||||
"C-"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
))?;
|
|
||||||
match self.code {
|
|
||||||
KeyCode::Backspace => f.write_str("backspace")?,
|
|
||||||
KeyCode::Enter => f.write_str("ret")?,
|
|
||||||
KeyCode::Left => f.write_str("left")?,
|
|
||||||
KeyCode::Right => f.write_str("right")?,
|
|
||||||
KeyCode::Up => f.write_str("up")?,
|
|
||||||
KeyCode::Down => f.write_str("down")?,
|
|
||||||
KeyCode::Home => f.write_str("home")?,
|
|
||||||
KeyCode::End => f.write_str("end")?,
|
|
||||||
KeyCode::PageUp => f.write_str("pageup")?,
|
|
||||||
KeyCode::PageDown => f.write_str("pagedown")?,
|
|
||||||
KeyCode::Tab => f.write_str("tab")?,
|
|
||||||
KeyCode::BackTab => f.write_str("backtab")?,
|
|
||||||
KeyCode::Delete => f.write_str("del")?,
|
|
||||||
KeyCode::Insert => f.write_str("ins")?,
|
|
||||||
KeyCode::Null => f.write_str("null")?,
|
|
||||||
KeyCode::Esc => f.write_str("esc")?,
|
|
||||||
KeyCode::Char('<') => f.write_str("lt")?,
|
|
||||||
KeyCode::Char('>') => f.write_str("gt")?,
|
|
||||||
KeyCode::Char('+') => f.write_str("plus")?,
|
|
||||||
KeyCode::Char('-') => f.write_str("minus")?,
|
|
||||||
KeyCode::Char(';') => f.write_str("semicolon")?,
|
|
||||||
KeyCode::Char('%') => f.write_str("percent")?,
|
|
||||||
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
|
|
||||||
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for KeyEvent {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut tokens: Vec<_> = s.split('-').collect();
|
|
||||||
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
|
|
||||||
"backspace" => KeyCode::Backspace,
|
|
||||||
"space" => KeyCode::Char(' '),
|
|
||||||
"ret" => KeyCode::Enter,
|
|
||||||
"lt" => KeyCode::Char('<'),
|
|
||||||
"gt" => KeyCode::Char('>'),
|
|
||||||
"plus" => KeyCode::Char('+'),
|
|
||||||
"minus" => KeyCode::Char('-'),
|
|
||||||
"semicolon" => KeyCode::Char(';'),
|
|
||||||
"percent" => KeyCode::Char('%'),
|
|
||||||
"left" => KeyCode::Left,
|
|
||||||
"right" => KeyCode::Right,
|
|
||||||
"up" => KeyCode::Down,
|
|
||||||
"home" => KeyCode::Home,
|
|
||||||
"end" => KeyCode::End,
|
|
||||||
"pageup" => KeyCode::PageUp,
|
|
||||||
"pagedown" => KeyCode::PageDown,
|
|
||||||
"tab" => KeyCode::Tab,
|
|
||||||
"backtab" => KeyCode::BackTab,
|
|
||||||
"del" => KeyCode::Delete,
|
|
||||||
"ins" => KeyCode::Insert,
|
|
||||||
"null" => KeyCode::Null,
|
|
||||||
"esc" => KeyCode::Esc,
|
|
||||||
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
|
|
||||||
function if function.len() > 1 && function.starts_with('F') => {
|
|
||||||
let function: String = function.chars().skip(1).collect();
|
|
||||||
let function = str::parse::<u8>(&function)?;
|
|
||||||
(function > 0 && function < 13)
|
|
||||||
.then(|| KeyCode::F(function))
|
|
||||||
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
|
|
||||||
}
|
|
||||||
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut modifiers = KeyModifiers::empty();
|
|
||||||
for token in tokens {
|
|
||||||
let flag = match token {
|
|
||||||
"S" => KeyModifiers::SHIFT,
|
|
||||||
"A" => KeyModifiers::ALT,
|
|
||||||
"C" => KeyModifiers::CONTROL,
|
|
||||||
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
|
|
||||||
};
|
|
||||||
|
|
||||||
if modifiers.contains(flag) {
|
|
||||||
return Err(anyhow!("Repeated key modifier '{}-'", token));
|
|
||||||
}
|
|
||||||
modifiers.insert(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(KeyEvent { code, modifiers })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for KeyEvent {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
s.parse().map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<event::KeyEvent> for KeyEvent {
|
|
||||||
fn from(event::KeyEvent { code, modifiers }: event::KeyEvent) -> KeyEvent {
|
|
||||||
KeyEvent { code, modifiers }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parsing_unmodified_keys() {
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("backspace").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Backspace,
|
|
||||||
modifiers: KeyModifiers::NONE
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("left").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Left,
|
|
||||||
modifiers: KeyModifiers::NONE
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>(",").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char(','),
|
|
||||||
modifiers: KeyModifiers::NONE
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("w").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char('w'),
|
|
||||||
modifiers: KeyModifiers::NONE
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("F12").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::F(12),
|
|
||||||
modifiers: KeyModifiers::NONE
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parsing_modified_keys() {
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("S-minus").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char('-'),
|
|
||||||
modifiers: KeyModifiers::SHIFT
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("C-A-S-F12").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::F(12),
|
|
||||||
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
str::parse::<KeyEvent>("S-C-2").unwrap(),
|
|
||||||
KeyEvent {
|
|
||||||
code: KeyCode::Char('2'),
|
|
||||||
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parsing_nonsensical_keys_fails() {
|
|
||||||
assert!(str::parse::<KeyEvent>("F13").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("F0").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("aaa").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("S-S-a").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("FU").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("123").is_err());
|
|
||||||
assert!(str::parse::<KeyEvent>("S--").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,17 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
|
||||||
|
pub mod clipboard;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod input;
|
|
||||||
pub mod register_selection;
|
pub mod register_selection;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
slotmap::new_key_type! {
|
use slotmap::new_key_type;
|
||||||
pub struct DocumentId;
|
new_key_type! { pub struct DocumentId; }
|
||||||
pub struct ViewId;
|
new_key_type! { pub struct ViewId; }
|
||||||
}
|
|
||||||
|
|
||||||
pub use document::Document;
|
pub use document::Document;
|
||||||
pub use editor::Editor;
|
pub use editor::Editor;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
@ -86,7 +91,84 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
/// Color theme for syntax highlighting.
|
/// Color theme for syntax highlighting.
|
||||||
#[derive(Debug)]
|
|
||||||
|
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
|
||||||
|
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Loader {
|
||||||
|
user_dir: PathBuf,
|
||||||
|
default_dir: PathBuf,
|
||||||
|
}
|
||||||
|
impl Loader {
|
||||||
|
/// Creates a new loader that can load themes from two directories.
|
||||||
|
pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
|
||||||
|
Self {
|
||||||
|
user_dir: user_dir.as_ref().join("themes"),
|
||||||
|
default_dir: default_dir.as_ref().join("themes"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a theme first looking in the `user_dir` then in `default_dir`
|
||||||
|
pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
|
||||||
|
if name == "default" {
|
||||||
|
return Ok(self.default());
|
||||||
|
}
|
||||||
|
let filename = format!("{}.toml", name);
|
||||||
|
|
||||||
|
let user_path = self.user_dir.join(&filename);
|
||||||
|
let path = if user_path.exists() {
|
||||||
|
user_path
|
||||||
|
} else {
|
||||||
|
self.default_dir.join(filename)
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = std::fs::read(&path)?;
|
||||||
|
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_names(path: &Path) -> Vec<String> {
|
||||||
|
std::fs::read_dir(path)
|
||||||
|
.map(|entries| {
|
||||||
|
entries
|
||||||
|
.filter_map(|entry| {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
if ext != "toml" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.trim_end_matches(".toml")
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all theme names available in default and user directory
|
||||||
|
pub fn names(&self) -> Vec<String> {
|
||||||
|
let mut names = Self::read_names(&self.user_dir);
|
||||||
|
names.extend(Self::read_names(&self.default_dir));
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default theme
|
||||||
|
pub fn default(&self) -> Theme {
|
||||||
|
DEFAULT_THEME.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
scopes: Vec<String>,
|
scopes: Vec<String>,
|
||||||
styles: HashMap<String, Style>,
|
styles: HashMap<String, Style>,
|
||||||
|
@ -434,6 +434,10 @@ pub fn focus_next(&mut self) {
|
|||||||
self.focus = key;
|
self.focus = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn area(&self) -> Rect {
|
||||||
|
self.area
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
Loading…
Reference in New Issue
Block a user