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
40 changed files with 1587 additions and 769 deletions
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
|
||||
with:
|
||||
command: test
|
||||
args: --locked
|
||||
args: --release --locked
|
||||
|
||||
- name: Build release binary
|
||||
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"
|
||||
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
|
@ -134,6 +140,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.3.2"
|
||||
|
@ -254,6 +266,7 @@ dependencies = [
|
|||
name = "helix-core"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"etcetera",
|
||||
"helix-syntax",
|
||||
"once_cell",
|
||||
|
@ -354,6 +367,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"toml",
|
||||
"url",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1057,6 +1071,16 @@ version = "0.10.2+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
- [Installation](./install.md)
|
||||
- [Usage](./usage.md)
|
||||
- [Configuration](./configuration.md)
|
||||
- [Themes](./themes.md)
|
||||
- [Keymap](./keymap.md)
|
||||
- [Key Remapping](./remapping.md)
|
||||
- [Hooks](./hooks.md)
|
||||
|
|
|
@ -1,97 +1,10 @@
|
|||
# Configuration
|
||||
|
||||
To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
|
||||
|
||||
## LSP
|
||||
|
||||
To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
|
||||
```toml
|
||||
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 @@
|
|||
| `;` | Collapse selection onto a single cursor |
|
||||
| `Alt-;` | Flip selection cursor and anchor |
|
||||
| `%` | Select entire file |
|
||||
| `x` | Select current line |
|
||||
| `X` | Extend to next line |
|
||||
| `[` | Expand selection to parent syntax node TODO: pick a key |
|
||||
| `x` | Select current line, if already selected, extend to next line |
|
||||
| `` | Expand selection to parent syntax node TODO: pick a key |
|
||||
| `J` | join lines inside selection |
|
||||
| `K` | keep selections matching the regex TODO: overlapped by hover help |
|
||||
| `Space` | keep only the primary selection TODO: overlapped by space mode |
|
||||
|
@ -155,10 +154,10 @@ This layer is similar to vim keybindings as kakoune does not support window.
|
|||
|
||||
| Key | Description |
|
||||
| ----- | ------------- |
|
||||
| `w`, `ctrl-w` | Switch to next window |
|
||||
| `v`, `ctrl-v` | Vertical right split |
|
||||
| `h`, `ctrl-h` | Horizontal bottom split |
|
||||
| `q`, `ctrl-q` | Close current window |
|
||||
| `w`, `Ctrl-w` | Switch to next window |
|
||||
| `v`, `Ctrl-v` | Vertical right split |
|
||||
| `h`, `Ctrl-h` | Horizontal bottom split |
|
||||
| `q`, `Ctrl-q` | Close current window |
|
||||
|
||||
## Space mode
|
||||
|
||||
|
@ -171,6 +170,11 @@ This layer is a kludge of mappings I had under leader key in neovim.
|
|||
| `s` | Open symbol picker (current document) |
|
||||
| `w` | Enter [window mode](#window-mode) |
|
||||
| `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
|
||||
|
||||
|
@ -184,4 +188,4 @@ Keys to use within picker.
|
|||
| `Enter` | Open selected |
|
||||
| `Ctrl-h` | Open horizontally |
|
||||
| `Ctrl-v` | Open vertically |
|
||||
| `Escape`, `ctrl-c` | Close picker |
|
||||
| `Escape`, `Ctrl-c` | Close picker |
|
||||
|
|
|
@ -22,27 +22,29 @@ A-x = "normal_mode" # Maps Alt-X to enter normal mode
|
|||
Control, Shift and Alt modifiers are encoded respectively with the prefixes
|
||||
`C-`, `S-` and `A-`. Special keys are encoded as follows:
|
||||
|
||||
* Backspace => "backspace"
|
||||
* Space => "space"
|
||||
* Return/Enter => "ret"
|
||||
* < => "lt"
|
||||
* \> => "gt"
|
||||
* \+ => "plus"
|
||||
* \- => "minus"
|
||||
* ; => "semicolon"
|
||||
* % => "percent"
|
||||
* Left => "left"
|
||||
* Right => "right"
|
||||
* Up => "up"
|
||||
* Home => "home"
|
||||
* End => "end"
|
||||
* Page Up => "pageup"
|
||||
* Page Down => "pagedown"
|
||||
* Tab => "tab"
|
||||
* Back Tab => "backtab"
|
||||
* Delete => "del"
|
||||
* Insert => "ins"
|
||||
* Null => "null"
|
||||
* Escape => "esc"
|
||||
| Key name | Representation |
|
||||
| --- | --- |
|
||||
| Backspace | `"backspace"` |
|
||||
| Space | `"space"` |
|
||||
| Return/Enter | `"ret"` |
|
||||
| < | `"lt"` |
|
||||
| \> | `"gt"` |
|
||||
| \+ | `"plus"` |
|
||||
| \- | `"minus"` |
|
||||
| ; | `"semicolon"` |
|
||||
| % | `"percent"` |
|
||||
| Left | `"left"` |
|
||||
| Right | `"right"` |
|
||||
| Up | `"up"` |
|
||||
| Home | `"home"` |
|
||||
| End | `"end"` |
|
||||
| Page | `"pageup"` |
|
||||
| Page | `"pagedown"` |
|
||||
| Tab | `"tab"` |
|
||||
| Back | `"backtab"` |
|
||||
| Delete | `"del"` |
|
||||
| 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"
|
||||
smallvec = "1.4"
|
||||
tendril = "0.4.2"
|
||||
unicode-segmentation = "1.7.1"
|
||||
unicode-segmentation = "1.7"
|
||||
unicode-width = "0.1"
|
||||
unicode-general-category = "0.4.0"
|
||||
unicode-general-category = "0.4"
|
||||
# slab = "0.4.2"
|
||||
tree-sitter = "0.19"
|
||||
once_cell = "1.8"
|
||||
arc-swap = "1"
|
||||
regex = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
@ -254,26 +254,23 @@ where
|
|||
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
let loader = Loader::new(
|
||||
Configuration {
|
||||
language: vec![LanguageConfiguration {
|
||||
scope: "source.rust".to_string(),
|
||||
file_types: vec!["rs".to_string()],
|
||||
language_id: Lang::Rust,
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
auto_format: false,
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
}],
|
||||
},
|
||||
Vec::new(),
|
||||
);
|
||||
let loader = Loader::new(Configuration {
|
||||
language: vec![LanguageConfiguration {
|
||||
scope: "source.rust".to_string(),
|
||||
file_types: vec!["rs".to_string()],
|
||||
language_id: Lang::Rust,
|
||||
highlight_config: OnceCell::new(),
|
||||
//
|
||||
roots: vec![],
|
||||
auto_format: false,
|
||||
language_server: None,
|
||||
indent: Some(IndentationConfiguration {
|
||||
tab_width: 4,
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
}],
|
||||
});
|
||||
|
||||
// set runtime path so we can find the queries
|
||||
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
|
|
@ -19,6 +19,12 @@ mod state;
|
|||
pub mod syntax;
|
||||
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> =
|
||||
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))]
|
||||
fn runtime_dir() -> std::path::PathBuf {
|
||||
pub fn runtime_dir() -> std::path::PathBuf {
|
||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||
return dir.into();
|
||||
}
|
||||
|
@ -98,8 +104,6 @@ pub use ropey::{Rope, RopeSlice};
|
|||
|
||||
pub use tendril::StrTendril as Tendril;
|
||||
|
||||
pub use unicode_general_category::get_general_category;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use {regex, tree_sitter};
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
|
||||
pub use helix_syntax::{get_language, get_language_name, Lang};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
|
@ -143,37 +145,49 @@ fn read_query(language: &str, filename: &str) -> String {
|
|||
}
|
||||
|
||||
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>> {
|
||||
self.highlight_config
|
||||
.get_or_init(|| {
|
||||
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))
|
||||
}
|
||||
})
|
||||
.get_or_init(|| self.initialize_highlight(scopes))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn is_highlight_initialized(&self) -> bool {
|
||||
self.highlight_config.get().is_some()
|
||||
}
|
||||
|
||||
pub fn indent_query(&self) -> Option<&IndentQuery> {
|
||||
self.indent_query
|
||||
.get_or_init(|| {
|
||||
|
@ -190,22 +204,18 @@ impl LanguageConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
pub static LOADER: OnceCell<Loader> = OnceCell::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Loader {
|
||||
// highlight_names ?
|
||||
language_configs: Vec<Arc<LanguageConfiguration>>,
|
||||
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
|
||||
scopes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
|
||||
pub fn new(config: Configuration) -> Self {
|
||||
let mut loader = Self {
|
||||
language_configs: Vec::new(),
|
||||
language_config_ids_by_file_type: HashMap::new(),
|
||||
scopes,
|
||||
};
|
||||
|
||||
for config in config.language {
|
||||
|
@ -225,10 +235,6 @@ impl Loader {
|
|||
loader
|
||||
}
|
||||
|
||||
pub fn scopes(&self) -> &[String] {
|
||||
&self.scopes
|
||||
}
|
||||
|
||||
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
|
||||
// Find all the language configurations that match this file name
|
||||
// or a suffix of the file name.
|
||||
|
@ -253,6 +259,10 @@ impl Loader {
|
|||
.find(|config| config.scope == scope)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
|
||||
self.language_configs.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TsParser {
|
||||
|
@ -772,7 +782,7 @@ pub struct HighlightConfiguration {
|
|||
combined_injections_query: Option<Query>,
|
||||
locals_pattern_index: usize,
|
||||
highlights_pattern_index: usize,
|
||||
highlight_indices: Vec<Option<Highlight>>,
|
||||
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
|
||||
non_local_variable_patterns: Vec<bool>,
|
||||
injection_content_capture_index: Option<u32>,
|
||||
injection_language_capture_index: Option<u32>,
|
||||
|
@ -924,7 +934,7 @@ impl HighlightConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
let highlight_indices = vec![None; query.capture_names().len()];
|
||||
let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
|
||||
Ok(Self {
|
||||
language,
|
||||
query,
|
||||
|
@ -957,17 +967,20 @@ impl HighlightConfiguration {
|
|||
///
|
||||
/// When highlighting, results are returned as `Highlight` values, which contain the index
|
||||
/// 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();
|
||||
self.highlight_indices.clear();
|
||||
self.highlight_indices
|
||||
.extend(self.query.capture_names().iter().map(move |capture_name| {
|
||||
let indices: Vec<_> = self
|
||||
.query
|
||||
.capture_names()
|
||||
.iter()
|
||||
.map(move |capture_name| {
|
||||
capture_parts.clear();
|
||||
capture_parts.extend(capture_name.split('.'));
|
||||
|
||||
let mut best_index = None;
|
||||
let mut best_match_len = 0;
|
||||
for (i, recognized_name) in recognized_names.iter().enumerate() {
|
||||
let recognized_name = recognized_name;
|
||||
let mut len = 0;
|
||||
let mut matches = true;
|
||||
for part in recognized_name.split('.') {
|
||||
|
@ -983,7 +996,10 @@ impl HighlightConfiguration {
|
|||
}
|
||||
}
|
||||
best_index.map(Highlight)
|
||||
}));
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.highlight_indices.store(Arc::new(indices));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1562,7 +1578,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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_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};
|
||||
|
||||
|
@ -14,7 +15,7 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Error;
|
||||
use anyhow::{Context, Error};
|
||||
|
||||
use crossterm::{
|
||||
event::{Event, EventStream},
|
||||
|
@ -36,6 +37,8 @@ pub struct Application {
|
|||
compositor: Compositor,
|
||||
editor: Editor,
|
||||
|
||||
theme_loader: Arc<theme::Loader>,
|
||||
syn_loader: Arc<syntax::Loader>,
|
||||
callbacks: LspCallbacks,
|
||||
|
||||
lsp_progress: LspProgressMap,
|
||||
|
@ -47,9 +50,36 @@ impl Application {
|
|||
use helix_view::editor::Action;
|
||||
let mut compositor = Compositor::new()?;
|
||||
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);
|
||||
|
||||
if !args.files.is_empty() {
|
||||
|
@ -72,10 +102,14 @@ impl Application {
|
|||
editor.new_file(Action::VerticalSplit);
|
||||
}
|
||||
|
||||
editor.set_theme(theme);
|
||||
|
||||
let mut app = Self {
|
||||
compositor,
|
||||
editor,
|
||||
|
||||
theme_loader,
|
||||
syn_loader,
|
||||
callbacks: FuturesUnordered::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
lsp_progress_enabled: config.global.lsp_progress,
|
||||
|
|
|
@ -11,7 +11,6 @@ use helix_core::{
|
|||
|
||||
use helix_view::{
|
||||
document::{IndentStyle, Mode},
|
||||
input::{KeyCode, KeyEvent},
|
||||
view::{View, PADDING},
|
||||
Document, DocumentId, Editor, ViewId,
|
||||
};
|
||||
|
@ -39,8 +38,8 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
|
||||
pub struct Context<'a> {
|
||||
pub selected_register: helix_view::RegisterSelection,
|
||||
|
@ -186,7 +185,6 @@ impl Command {
|
|||
search_next,
|
||||
extend_search_next,
|
||||
search_selection,
|
||||
select_line,
|
||||
extend_line,
|
||||
delete_selection,
|
||||
change_selection,
|
||||
|
@ -223,9 +221,14 @@ impl Command {
|
|||
undo,
|
||||
redo,
|
||||
yank,
|
||||
yank_joined_to_clipboard,
|
||||
yank_main_selection_to_clipboard,
|
||||
replace_with_yanked,
|
||||
replace_selections_with_clipboard,
|
||||
paste_after,
|
||||
paste_before,
|
||||
paste_clipboard_after,
|
||||
paste_clipboard_before,
|
||||
indent,
|
||||
unindent,
|
||||
format_selections,
|
||||
|
@ -253,48 +256,6 @@ impl Command {
|
|||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
let count = cx.count();
|
||||
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) {
|
||||
let count = cx.count();
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
@ -1318,6 +1264,57 @@ mod cmd {
|
|||
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] = &[
|
||||
TypableCommand {
|
||||
name: "quit",
|
||||
|
@ -1431,7 +1428,55 @@ mod cmd {
|
|||
fun: force_quit_all,
|
||||
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(|| {
|
||||
|
@ -2424,6 +2469,52 @@ fn yank(cx: &mut Context) {
|
|||
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)]
|
||||
enum Paste {
|
||||
Before,
|
||||
|
@ -2469,6 +2560,31 @@ fn paste_impl(
|
|||
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) {
|
||||
let reg_name = cx.selected_register.name();
|
||||
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 before selected text
|
||||
// R => replace selected text with yanked text
|
||||
|
@ -2854,7 +2993,7 @@ fn hover(cx: &mut Context) {
|
|||
|
||||
// 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);
|
||||
compositor.push(Box::new(popup));
|
||||
}
|
||||
|
@ -3009,6 +3148,11 @@ fn space_mode(cx: &mut Context) {
|
|||
'b' => buffer_picker(cx),
|
||||
's' => symbol_picker(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),
|
||||
// TODO: temporary since space mode took its old key
|
||||
' ' => 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.
|
||||
///
|
||||
/// Can be used before `Box::downcast()`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// ```
|
||||
//
|
||||
// # Examples
|
||||
//
|
||||
// ```rust
|
||||
// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
|
||||
// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
|
||||
// ```
|
||||
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 crate::keymap::Keymaps;
|
||||
use serde::{de::Error as SerdeError, Deserialize, Serialize};
|
||||
|
||||
use crate::keymap::{parse_keymaps, Keymaps};
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
pub struct GlobalConfig {
|
||||
pub theme: Option<String>,
|
||||
pub lsp_progress: bool,
|
||||
}
|
||||
|
||||
impl Default for GlobalConfig {
|
||||
fn default() -> Self {
|
||||
Self { lsp_progress: true }
|
||||
Self {
|
||||
lsp_progress: true,
|
||||
theme: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[derive(Default)]
|
||||
pub struct Config {
|
||||
pub global: GlobalConfig,
|
||||
pub keys: Keymaps,
|
||||
pub keymaps: Keymaps,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_keymaps_config_file() {
|
||||
use helix_core::hashmap;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
let sample_keymaps = r#"
|
||||
[keys.insert]
|
||||
y = "move_line_down"
|
||||
S-C-a = "delete_selection"
|
||||
|
||||
[keys.normal]
|
||||
A-F12 = "move_next_word_end"
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
toml::from_str::<Config>(sample_keymaps).unwrap(),
|
||||
Config {
|
||||
global: Default::default(),
|
||||
keys: Keymaps(hashmap! {
|
||||
Mode::Insert => hashmap! {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('y'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
} => Command::move_line_down,
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
);
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct TomlConfig {
|
||||
theme: Option<String>,
|
||||
lsp_progress: Option<bool>,
|
||||
keys: Option<HashMap<String, HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let config = TomlConfig::deserialize(deserializer)?;
|
||||
Ok(Self {
|
||||
global: GlobalConfig {
|
||||
lsp_progress: config.lsp_progress.unwrap_or(true),
|
||||
theme: config.theme,
|
||||
},
|
||||
keymaps: config
|
||||
.keys
|
||||
.map(|r| parse_keymaps(&r))
|
||||
.transpose()
|
||||
.map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))?
|
||||
.unwrap_or_else(Keymaps::default),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ pub use crate::commands::Command;
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use helix_core::hashmap;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
|
@ -101,6 +99,14 @@ use std::{
|
|||
// 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_rules! key {
|
||||
($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 {
|
||||
fn default() -> Keymaps {
|
||||
let normal = hashmap!(
|
||||
fn default() -> Self {
|
||||
let normal = Keymap(hashmap!(
|
||||
key!('h') => Command::move_char_left,
|
||||
key!('j') => Command::move_line_down,
|
||||
key!('k') => Command::move_line_up,
|
||||
|
@ -202,9 +196,7 @@ impl Default for Keymaps {
|
|||
key!(';') => Command::collapse_selection,
|
||||
alt!(';') => Command::flip_selections,
|
||||
key!('%') => Command::select_all,
|
||||
key!('x') => Command::select_line,
|
||||
key!('X') => Command::extend_line,
|
||||
// or select mode X?
|
||||
key!('x') => Command::extend_line,
|
||||
// extend_to_whole_line, crop_to_whole_line
|
||||
|
||||
|
||||
|
@ -283,12 +275,12 @@ impl Default for Keymaps {
|
|||
key!('z') => Command::view_mode,
|
||||
|
||||
key!('"') => Command::select_register,
|
||||
);
|
||||
));
|
||||
// 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
|
||||
// because some selection operations can now be done from normal mode, some from select mode.
|
||||
let mut select = normal.clone();
|
||||
select.extend(
|
||||
select.0.extend(
|
||||
hashmap!(
|
||||
key!('h') => Command::extend_char_left,
|
||||
key!('j') => Command::extend_line_down,
|
||||
|
@ -321,7 +313,7 @@ impl Default for Keymaps {
|
|||
// TODO: select could be normal mode with some bindings merged over
|
||||
Mode::Normal => normal,
|
||||
Mode::Select => select,
|
||||
Mode::Insert => hashmap!(
|
||||
Mode::Insert => Keymap(hashmap!(
|
||||
key!(Esc) => Command::normal_mode as Command,
|
||||
key!(Backspace) => Command::delete_char_backward,
|
||||
key!(Delete) => Command::delete_char_forward,
|
||||
|
@ -333,9 +325,313 @@ impl Default for Keymaps {
|
|||
key!(Right) => Command::move_char_right,
|
||||
key!(PageUp) => Command::page_up,
|
||||
key!(PageDown) => Command::page_down,
|
||||
key!(Home) => Command::move_line_start,
|
||||
key!(End) => Command::move_line_end,
|
||||
ctrl!('x') => Command::completion,
|
||||
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::args::Args;
|
||||
use helix_term::config::Config;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
let mut base_config = fern::Dispatch::new();
|
||||
|
||||
|
@ -88,11 +89,12 @@ FLAGS:
|
|||
std::fs::create_dir_all(&conf_dir).ok();
|
||||
}
|
||||
|
||||
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
|
||||
Ok(config) => toml::from_str(&config)?,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
|
||||
Err(err) => return Err(Error::new(err)),
|
||||
};
|
||||
let config = std::fs::read_to_string(conf_dir.join("config.toml"))
|
||||
.ok()
|
||||
.map(|s| toml::from_str(&s))
|
||||
.transpose()?
|
||||
.or_else(|| Some(Config::default()))
|
||||
.unwrap();
|
||||
|
||||
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||
|
||||
|
|
|
@ -238,6 +238,9 @@ impl Component for Completion {
|
|||
.language()
|
||||
.and_then(|scope| scope.strip_prefix("source."))
|
||||
.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 {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
|
@ -246,42 +249,60 @@ impl Component for Completion {
|
|||
value: contents,
|
||||
})) => {
|
||||
// TODO: convert to wrapped text
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
))
|
||||
Markdown::new(
|
||||
format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
),
|
||||
cx.editor.syn_loader.clone(),
|
||||
)
|
||||
}
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: contents,
|
||||
})) => {
|
||||
// TODO: set language based on doc scope
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
))
|
||||
Markdown::new(
|
||||
format!(
|
||||
"```{}\n{}\n```\n{}",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
contents.clone()
|
||||
),
|
||||
cx.editor.syn_loader.clone(),
|
||||
)
|
||||
}
|
||||
None if option.detail.is_some() => {
|
||||
// TODO: copied from above
|
||||
|
||||
// TODO: set language based on doc scope
|
||||
Markdown::new(format!(
|
||||
"```{}\n{}\n```",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
))
|
||||
Markdown::new(
|
||||
format!(
|
||||
"```{}\n{}\n```",
|
||||
language,
|
||||
option.detail.as_deref().unwrap_or_default(),
|
||||
),
|
||||
cx.editor.syn_loader.clone(),
|
||||
)
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
let half = area.height / 2;
|
||||
let height = 15.min(half);
|
||||
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
|
||||
let area = Rect::new(0, area.height - height - 2, area.width, height);
|
||||
// we want to make sure the cursor is visible (not hidden behind the documentation)
|
||||
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
|
||||
let background = cx.editor.theme.get("ui.popup");
|
||||
|
|
|
@ -11,13 +11,12 @@ use helix_core::{
|
|||
syntax::{self, HighlightEvent},
|
||||
LineEnding, Position, Range,
|
||||
};
|
||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{read, Event, EventStream},
|
||||
event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
||||
};
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
|
@ -130,7 +129,7 @@ impl EditorView {
|
|||
})],
|
||||
};
|
||||
let mut spans = Vec::new();
|
||||
let mut visual_x = 0;
|
||||
let mut visual_x = 0u16;
|
||||
let mut line = 0u16;
|
||||
let tab_width = doc.tab_width();
|
||||
|
||||
|
@ -186,7 +185,7 @@ impl EditorView {
|
|||
break 'outer;
|
||||
}
|
||||
} else if grapheme == "\t" {
|
||||
visual_x += (tab_width as u16);
|
||||
visual_x = visual_x.saturating_add(tab_width as u16);
|
||||
} else {
|
||||
let out_of_bounds = visual_x < view.first_col as u16
|
||||
|| visual_x >= viewport.width + view.first_col as u16;
|
||||
|
@ -198,7 +197,7 @@ impl EditorView {
|
|||
|
||||
if out_of_bounds {
|
||||
// if we're offscreen just keep going until we hit a new line
|
||||
visual_x += width;
|
||||
visual_x = visual_x.saturating_add(width);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -608,8 +607,7 @@ impl Component for EditorView {
|
|||
cx.editor.resize(Rect::new(0, 0, width, height - 1));
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(key) => {
|
||||
let mut key = KeyEvent::from(key);
|
||||
Event::Key(mut key) => {
|
||||
canonicalize_key(&mut key);
|
||||
// clear status
|
||||
cx.editor.status_msg = None;
|
||||
|
|
|
@ -7,25 +7,34 @@ use tui::{
|
|||
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};
|
||||
|
||||
pub struct Markdown {
|
||||
contents: String,
|
||||
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
}
|
||||
|
||||
// TODO: pre-render and self reference via Pin
|
||||
// better yet, just use Tendril + subtendril for references
|
||||
|
||||
impl Markdown {
|
||||
pub fn new(contents: String) -> Self {
|
||||
Self { contents }
|
||||
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||
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 tui::text::{Span, Spans, Text};
|
||||
|
||||
|
@ -79,9 +88,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
|||
use helix_core::Rope;
|
||||
|
||||
let rope = Rope::from(text.as_ref());
|
||||
let syntax = syntax::LOADER
|
||||
.get()
|
||||
.unwrap()
|
||||
let syntax = loader
|
||||
.language_config_for_scope(&format!("source.{}", language))
|
||||
.and_then(|config| config.highlight_config(theme.scopes()))
|
||||
.map(|config| Syntax::new(&rope, config));
|
||||
|
@ -101,9 +108,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
|||
}
|
||||
HighlightEvent::Source { start, end } => {
|
||||
let style = match highlights.first() {
|
||||
Some(span) => {
|
||||
theme.get(theme.scopes()[span.0].as_str())
|
||||
}
|
||||
Some(span) => theme.get(&theme.scopes()[span.0]),
|
||||
None => text_style,
|
||||
};
|
||||
|
||||
|
@ -159,7 +164,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
|
|||
}
|
||||
}
|
||||
Event::Code(text) | Event::Html(text) => {
|
||||
log::warn!("code {:?}", text);
|
||||
let mut span = to_span(text);
|
||||
span.style = code_style;
|
||||
spans.push(span);
|
||||
|
@ -198,7 +202,7 @@ impl Component for Markdown {
|
|||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
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)
|
||||
.wrap(Wrap { trim: false })
|
||||
|
@ -209,7 +213,7 @@ impl Component for Markdown {
|
|||
}
|
||||
|
||||
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 width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
|
||||
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 {
|
||||
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 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.
|
||||
pub fn filename(input: &str) -> Vec<Completion> {
|
||||
// Rust's filename handling is really annoying.
|
||||
|
@ -178,10 +211,6 @@ pub mod completers {
|
|||
|
||||
// if empty, return a list of dirs and files in current dir
|
||||
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();
|
||||
|
||||
// inefficient, but we need to calculate the scores, filter out None, then sort.
|
||||
|
|
|
@ -6,6 +6,11 @@ use helix_view::{Editor, Theme};
|
|||
use std::{borrow::Cow, ops::RangeFrom};
|
||||
use tui::terminal::CursorKind;
|
||||
|
||||
use helix_core::{
|
||||
unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
|
||||
unicode::width::UnicodeWidthStr,
|
||||
};
|
||||
|
||||
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
||||
|
||||
pub struct Prompt {
|
||||
|
@ -34,6 +39,17 @@ pub enum CompletionDirection {
|
|||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Movement {
|
||||
BackwardChar(usize),
|
||||
BackwardWord(usize),
|
||||
ForwardChar(usize),
|
||||
ForwardWord(usize),
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub fn new(
|
||||
prompt: String,
|
||||
|
@ -52,30 +68,120 @@ impl Prompt {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let pos = if self.line.is_empty() {
|
||||
0
|
||||
} else {
|
||||
self.line
|
||||
.char_indices()
|
||||
.nth(self.cursor)
|
||||
.map(|(pos, _)| pos)
|
||||
.unwrap_or_else(|| self.line.len())
|
||||
};
|
||||
self.line.insert(pos, c);
|
||||
self.cursor += 1;
|
||||
self.line.insert(self.cursor, c);
|
||||
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
|
||||
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
|
||||
self.cursor = pos;
|
||||
}
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
pub fn move_char_left(&mut self) {
|
||||
self.cursor = self.cursor.saturating_sub(1)
|
||||
}
|
||||
|
||||
pub fn move_char_right(&mut self) {
|
||||
if self.cursor < self.line.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
pub fn move_cursor(&mut self, movement: Movement) {
|
||||
let pos = self.eval_movement(movement);
|
||||
self.cursor = pos
|
||||
}
|
||||
|
||||
pub fn move_start(&mut self) {
|
||||
|
@ -87,39 +193,29 @@ impl Prompt {
|
|||
}
|
||||
|
||||
pub fn delete_char_backwards(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
let pos = self
|
||||
.line
|
||||
.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);
|
||||
}
|
||||
let pos = self.eval_movement(Movement::BackwardChar(1));
|
||||
self.line.replace_range(pos..self.cursor, "");
|
||||
self.cursor = pos;
|
||||
|
||||
self.exit_selection();
|
||||
self.completion = (self.completion_fn)(&self.line);
|
||||
}
|
||||
|
||||
pub fn delete_word_backwards(&mut self) {
|
||||
use helix_core::get_general_category;
|
||||
let mut chars = self.line.char_indices().rev();
|
||||
// TODO add skipping whitespace logic here
|
||||
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);
|
||||
let pos = self.eval_movement(Movement::BackwardWord(1));
|
||||
self.line.replace_range(pos..self.cursor, "");
|
||||
self.cursor = pos;
|
||||
|
||||
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) {
|
||||
|
@ -293,31 +389,71 @@ impl Component for Prompt {
|
|||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
} => {
|
||||
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
|
||||
return close_fn;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('f'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Right,
|
||||
..
|
||||
} => self.move_char_right(),
|
||||
} => self.move_cursor(Movement::ForwardChar(1)),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Left,
|
||||
..
|
||||
} => self.move_char_left(),
|
||||
} => self.move_cursor(Movement::BackwardChar(1)),
|
||||
KeyEvent {
|
||||
code: KeyCode::End,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('e'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_end(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Home,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('a'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => 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 {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.delete_word_backwards(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('k'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.kill_to_end_of_line(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
@ -363,7 +499,9 @@ impl Component for Prompt {
|
|||
(
|
||||
Some(Position::new(
|
||||
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,
|
||||
)
|
||||
|
|
|
@ -203,16 +203,6 @@ impl Buffer {
|
|||
/// # Panics
|
||||
///
|
||||
/// 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 {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
|
@ -245,15 +235,6 @@ impl Buffer {
|
|||
/// # Panics
|
||||
///
|
||||
/// 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) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
|
@ -510,6 +491,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
#[cfg(debug_assertions)]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
@ -520,6 +502,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
#[cfg(debug_assertions)]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
//! implement your own.
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! 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
|
||||
//! 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"] }
|
||||
toml = "0.5"
|
||||
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 serde::de::{self, Deserialize, Deserializer};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
@ -12,12 +10,14 @@ use helix_core::{
|
|||
auto_detect_line_ending,
|
||||
chars::{char_is_line_ending, char_is_whitespace},
|
||||
history::History,
|
||||
syntax::{LanguageConfiguration, LOADER},
|
||||
syntax::{self, LanguageConfiguration},
|
||||
ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
|
||||
DEFAULT_LINE_ENDING,
|
||||
};
|
||||
|
||||
use crate::{DocumentId, ViewId};
|
||||
use crate::{DocumentId, Theme, ViewId};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Mode {
|
||||
|
@ -26,40 +26,6 @@ pub enum Mode {
|
|||
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)]
|
||||
pub enum IndentStyle {
|
||||
Tabs,
|
||||
|
@ -127,6 +93,29 @@ impl fmt::Debug for Document {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
/// original value.
|
||||
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
|
||||
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
|
||||
/// needs to improve on.
|
||||
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
|
||||
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
let path = expand_tilde(path);
|
||||
let mut components = path.components().peekable();
|
||||
|
@ -253,7 +242,11 @@ impl Document {
|
|||
}
|
||||
|
||||
// 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};
|
||||
|
||||
let mut doc = if !path.exists() {
|
||||
|
@ -277,6 +270,10 @@ impl Document {
|
|||
doc.detect_indent_style();
|
||||
doc.set_line_ending(line_ending);
|
||||
|
||||
if let Some(loader) = config_loader {
|
||||
doc.detect_language(theme, loader);
|
||||
}
|
||||
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
|
@ -351,12 +348,10 @@ impl Document {
|
|||
}
|
||||
}
|
||||
|
||||
fn detect_language(&mut self) {
|
||||
if let Some(path) = self.path() {
|
||||
let loader = LOADER.get().unwrap();
|
||||
let language_config = loader.language_config_for_file_name(path);
|
||||
let scopes = loader.scopes();
|
||||
self.set_language(language_config, scopes);
|
||||
pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
|
||||
if let Some(path) = &self.path {
|
||||
let language_config = config_loader.language_config_for_file_name(path);
|
||||
self.set_language(theme, language_config);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -493,18 +488,16 @@ impl Document {
|
|||
// and error out when document is saved
|
||||
self.path = Some(path);
|
||||
|
||||
// try detecting the language based on filepath
|
||||
self.detect_language();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_language(
|
||||
&mut self,
|
||||
theme: Option<&Theme>,
|
||||
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
|
||||
scopes: &[String],
|
||||
) {
|
||||
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) {
|
||||
let syntax = Syntax::new(&self.text, highlight_config);
|
||||
self.syntax = Some(syntax);
|
||||
|
@ -518,12 +511,15 @@ impl Document {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn set_language2(&mut self, scope: &str) {
|
||||
let loader = LOADER.get().unwrap();
|
||||
let language_config = loader.language_config_for_scope(scope);
|
||||
let scopes = loader.scopes();
|
||||
pub fn set_language2(
|
||||
&mut self,
|
||||
scope: &str,
|
||||
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>>) {
|
||||
|
|
|
@ -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::terminal::CursorKind;
|
||||
|
||||
use futures_util::future;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
|
@ -23,6 +28,10 @@ pub struct Editor {
|
|||
pub registers: Registers,
|
||||
pub theme: Theme,
|
||||
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)>,
|
||||
}
|
||||
|
@ -35,27 +44,11 @@ pub enum Action {
|
|||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new(mut area: tui::layout::Rect) -> Self {
|
||||
use helix_core::config_dir;
|
||||
let config = std::fs::read(config_dir().join("theme.toml"));
|
||||
// load $HOME/.config/helix/theme.toml, fallback to default config
|
||||
let toml = config
|
||||
.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()));
|
||||
|
||||
pub fn new(
|
||||
mut area: tui::layout::Rect,
|
||||
themes: Arc<theme::Loader>,
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
) -> Self {
|
||||
let language_servers = helix_lsp::Registry::new();
|
||||
|
||||
// HAXX: offset the render area height by 1 to account for prompt/commandline
|
||||
|
@ -66,9 +59,12 @@ impl Editor {
|
|||
documents: SlotMap::with_key(),
|
||||
count: None,
|
||||
selected_register: RegisterSelection::default(),
|
||||
theme,
|
||||
theme: themes.default(),
|
||||
language_servers,
|
||||
syn_loader: config_loader,
|
||||
theme_loader: themes,
|
||||
registers: Registers::default(),
|
||||
clipboard_provider: get_clipboard_provider(),
|
||||
status_msg: None,
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +81,32 @@ impl Editor {
|
|||
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) {
|
||||
for (view, _) in self.tree.views_mut() {
|
||||
let doc = &self.documents[view.doc];
|
||||
|
@ -168,7 +190,7 @@ impl Editor {
|
|||
let id = if let Some(id) = id {
|
||||
id
|
||||
} 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
|
||||
let language_server = doc
|
||||
|
@ -254,6 +276,10 @@ impl Editor {
|
|||
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 {
|
||||
// let id = self.view().doc;
|
||||
// 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]
|
||||
pub mod macros;
|
||||
|
||||
pub mod clipboard;
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
pub mod input;
|
||||
pub mod register_selection;
|
||||
pub mod theme;
|
||||
pub mod tree;
|
||||
pub mod view;
|
||||
|
||||
slotmap::new_key_type! {
|
||||
pub struct DocumentId;
|
||||
pub struct ViewId;
|
||||
}
|
||||
use slotmap::new_key_type;
|
||||
new_key_type! { pub struct DocumentId; }
|
||||
new_key_type! { pub struct ViewId; }
|
||||
|
||||
pub use document::Document;
|
||||
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 once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use toml::Value;
|
||||
|
||||
|
@ -86,7 +91,84 @@ pub use tui::style::{Color, Modifier, Style};
|
|||
// }
|
||||
|
||||
/// 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 {
|
||||
scopes: Vec<String>,
|
||||
styles: HashMap<String, Style>,
|
||||
|
|
|
@ -434,6 +434,10 @@ impl Tree {
|
|||
self.focus = key;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(&self) -> Rect {
|
||||
self.area
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
Loading…
Add table
Reference in a new issue