summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/blank_issue.md4
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md13
-rw-r--r--.github/workflows/release.yml2
-rw-r--r--Cargo.lock24
-rw-r--r--book/src/SUMMARY.md1
-rw-r--r--book/src/configuration.md91
-rw-r--r--book/src/keymap.md20
-rw-r--r--book/src/remapping.md48
-rw-r--r--book/src/themes.md94
l---------contrib/themes1
-rw-r--r--helix-core/Cargo.toml5
-rw-r--r--helix-core/src/indent.rs37
-rw-r--r--helix-core/src/lib.rs10
-rw-r--r--helix-core/src/syntax.rs98
-rw-r--r--helix-term/src/application.rs44
-rw-r--r--helix-term/src/commands.rs294
-rw-r--r--helix-term/src/compositor.rs14
-rw-r--r--helix-term/src/config.rs84
-rw-r--r--helix-term/src/keymap.rs342
-rw-r--r--helix-term/src/main.rs14
-rw-r--r--helix-term/src/ui/completion.rs59
-rw-r--r--helix-term/src/ui/editor.rs12
-rw-r--r--helix-term/src/ui/markdown.rs32
-rw-r--r--helix-term/src/ui/mod.rs39
-rw-r--r--helix-term/src/ui/prompt.rs236
-rw-r--r--helix-tui/src/buffer.rs21
-rw-r--r--helix-tui/src/lib.rs2
-rw-r--r--helix-tui/src/widgets/mod.rs2
-rw-r--r--helix-view/Cargo.toml3
-rw-r--r--helix-view/src/clipboard.rs193
-rw-r--r--helix-view/src/document.rs106
-rw-r--r--helix-view/src/editor.rs78
-rw-r--r--helix-view/src/input.rs226
-rw-r--r--helix-view/src/lib.rs9
-rw-r--r--helix-view/src/theme.rs86
-rw-r--r--helix-view/src/tree.rs4
-rw-r--r--runtime/themes/README.md (renamed from contrib/themes/README.md)0
-rw-r--r--runtime/themes/bogster.toml (renamed from contrib/themes/bogster.toml)0
-rw-r--r--runtime/themes/ingrid.toml (renamed from contrib/themes/ingrid.toml)0
-rw-r--r--runtime/themes/onedark.toml (renamed from contrib/themes/onedark.toml)0
40 files changed, 1583 insertions, 765 deletions
diff --git a/.github/ISSUE_TEMPLATE/blank_issue.md b/.github/ISSUE_TEMPLATE/blank_issue.md
new file mode 100644
index 00000000..9aef3ebe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/blank_issue.md
@@ -0,0 +1,4 @@
+---
+name: Blank Issue
+about: Create a blank issue.
+---
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..43ba412b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -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
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7d6ffd43..afb9a7b0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/Cargo.lock b/Cargo.lock
index a1de7138..e90f5482 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -135,6 +141,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -1058,6 +1072,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 3ea1fb9a..5dea3112 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -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)
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 51a08e03..087d3fbb 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.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.
-
diff --git a/book/src/keymap.md b/book/src/keymap.md
index aee4b3a4..1e159f81 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -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 |
diff --git a/book/src/remapping.md b/book/src/remapping.md
index 610d5179..1b724be7 100644
--- a/book/src/remapping.md
+++ b/book/src/remapping.md
@@ -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)
diff --git a/book/src/themes.md b/book/src/themes.md
new file mode 100644
index 00000000..80fee3d7
--- /dev/null
+++ b/book/src/themes.md
@@ -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.
diff --git a/contrib/themes b/contrib/themes
new file mode 120000
index 00000000..d09bf827
--- /dev/null
+++ b/contrib/themes
@@ -0,0 +1 @@
+../runtime/themes \ No newline at end of file
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index 13ac35fb..bab062e1 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -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"] }
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 58124ed2..8e0379e2 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -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"));
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 183b9f0a..69294688 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -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};
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 92e52d73..63ca424e 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -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 {
- 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();
+ 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 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 injections_query = read_query(&language, "injections.scm");
- let locals_query = "";
+ 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))
- }
- })
+ 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(|| 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.
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index ce43808a..08853ed0 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -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,
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index b006504b..28c4fe3a 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -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()
+ }
+}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 6b39bb62..0e6a313d 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -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>;
}
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index 9c962299..2c95fae3 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -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"
- "#;
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+struct TomlConfig {
+ theme: Option<String>,
+ lsp_progress: Option<bool>,
+ keys: Option<HashMap<String, HashMap<String, String>>>,
+}
- 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,
- },
- })
- }
- );
+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),
+ })
+ }
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 24924832..46d495c3 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -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());
+ }
+}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 12176910..ef912480 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -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")?;
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 06ed966d..80f7d590 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -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");
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index da8f0f53..faede58c 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -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;
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 3ce3a5b8..72a3e4ff 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -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);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 39e11cd6..e0177b7c 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -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.
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 991b328d..7ca4308c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -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..);
+ 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,32 +389,72 @@ 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,
)
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index c584ee7f..0d1edc46 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -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);
diff --git a/helix-tui/src/lib.rs b/helix-tui/src/lib.rs
index 0d466f8b..05263bc8 100644
--- a/helix-tui/src/lib.rs
+++ b/helix-tui/src/lib.rs
@@ -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:
diff --git a/helix-tui/src/widgets/mod.rs b/helix-tui/src/widgets/mod.rs
index e334b894..484ad50e 100644
--- a/helix-tui/src/widgets/mod.rs
+++ b/helix-tui/src/widgets/mod.rs
@@ -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.
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 7f18e9a2..8d93d2d9 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -34,3 +34,6 @@ slotmap = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = "~0.4"
+
+which = "4.1"
+
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs
new file mode 100644
index 00000000..dcc44340
--- /dev/null
+++ b/helix-view/src/clipboard.rs
@@ -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(|_| ())
+ }
+ }
+}
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 3e38c24d..9326fb79 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -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>>) {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index fb2eb36d..839bcdcd 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -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];
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
deleted file mode 100644
index ab417819..00000000
--- a/helix-view/src/input.rs
+++ /dev/null
@@ -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());
- }
-}
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index 8b635700..17f415fc 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -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;
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 51a21421..66b91294 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -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>,
diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs
index a0c466d9..f7d6c1f2 100644
--- a/helix-view/src/tree.rs
+++ b/helix-view/src/tree.rs
@@ -434,6 +434,10 @@ impl Tree {
self.focus = key;
}
}
+
+ pub fn area(&self) -> Rect {
+ self.area
+ }
}
#[derive(Debug)]
diff --git a/contrib/themes/README.md b/runtime/themes/README.md
index 1c9c5ae9..1c9c5ae9 100644
--- a/contrib/themes/README.md
+++ b/runtime/themes/README.md
diff --git a/contrib/themes/bogster.toml b/runtime/themes/bogster.toml
index 43b422f3..43b422f3 100644
--- a/contrib/themes/bogster.toml
+++ b/runtime/themes/bogster.toml
diff --git a/contrib/themes/ingrid.toml b/runtime/themes/ingrid.toml
index d32a89d1..d32a89d1 100644
--- a/contrib/themes/ingrid.toml
+++ b/runtime/themes/ingrid.toml
diff --git a/contrib/themes/onedark.toml b/runtime/themes/onedark.toml
index 65f26725..65f26725 100644
--- a/contrib/themes/onedark.toml
+++ b/runtime/themes/onedark.toml