on master help sign in
Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection

Rebasing was making me manually fix conflicts on every commit, so merging instead.

commite686c3e4626fdafbcc2dab9d381eba83a5f6f974 authorNathan Vegdahl <cessen@cessen.com> dateSun 20 Jun 2021, 16:09 (5 years ago) parent4efd6719856257
40 files changed+1583−765
.github/ISSUE_TEMPLATE/blank_issue.md+4 .github/ISSUE_TEMPLATE/feature_request.md+13 .github/workflows/release.yml+1−1 Cargo.lock+24 book/src/SUMMARY.md+1 book/src/configuration.md+2−89 book/src/keymap.md+12−8 book/src/remapping.md+25−23 book/src/themes.md+94 contrib/themes+1 helix-core/Cargo.toml+3−2 helix-core/src/indent.rs+17−20 helix-core/src/lib.rs+7−3 helix-core/src/syntax.rs+57−41 helix-term/src/application.rs+39−5 helix-term/src/commands.rs+232−62 helix-term/src/compositor.rs+7−7 helix-term/src/config.rs+38−46 helix-term/src/keymap.rs+319−23 helix-term/src/main.rs+8−6 helix-term/src/ui/completion.rs+40−19 helix-term/src/ui/editor.rs+5−7 helix-term/src/ui/markdown.rs+18−14 helix-term/src/ui/mod.rs+34−5 helix-term/src/ui/prompt.rs+187−49 helix-tui/src/buffer.rs+2−19 helix-tui/src/lib.rs+1−1 helix-tui/src/widgets/mod.rs+1−1 helix-view/Cargo.toml+3 helix-view/src/clipboard.rs+193 helix-view/src/document.rs+51−55 helix-view/src/editor.rs+52−26 helix-view/src/input.rs−226 helix-view/src/lib.rs+4−5 helix-view/src/theme.rs+84−2 helix-view/src/tree.rs+4 contrib/themes/README.md → runtime/themes/README.md contrib/themes/bogster.toml → runtime/themes/bogster.toml contrib/themes/ingrid.toml → runtime/themes/ingrid.toml contrib/themes/onedark.toml → runtime/themes/onedark.toml
.github/ISSUE_TEMPLATE/blank_issue.mdnew file+4
@@ -0,0 +1,4 @@
1+---
2+name: Blank Issue
3+about: Create a blank issue.
4+---
.github/ISSUE_TEMPLATE/feature_request.mdnew file+13
@@ -0,0 +1,13 @@
1+---
2+name: Feature request
3+about: Suggest a new feature or improvement
4+title: ''
5+labels: C-enchancement
6+assignees: ''
7+---
8+
9+<!-- Your feature may already be reported!
10+Please search on the issue tracker before creating one. -->
11+
12+#### Describe your feature request
13+
.github/workflows/release.yml+1−1
@@ -68,7 +68,7 @@ jobs:
6868 uses: actions-rs/cargo@v1
6969 with:
7070 command: test
71− args: --locked
71+ args: --release --locked
7272
7373 - name: Build release binary
7474 uses: actions-rs/cargo@v1
Cargo.lock+24
@@ -17,6 +17,12 @@ version = "1.0.41"
1717source = "registry+https://github.com/rust-lang/crates.io-index"
1818checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
1919
20+[[package]]
21+name = "arc-swap"
22+version = "1.3.0"
23+source = "registry+https://github.com/rust-lang/crates.io-index"
24+checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
25+
2026[[package]]
2127name = "autocfg"
2228version = "1.0.1"
@@ -134,6 +140,12 @@ dependencies = [
134140 "winapi",
135141]
136142
143+[[package]]
144+name = "either"
145+version = "1.6.1"
146+source = "registry+https://github.com/rust-lang/crates.io-index"
147+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
148+
137149[[package]]
138150name = "etcetera"
139151version = "0.3.2"
@@ -254,6 +266,7 @@ dependencies = [
254266name = "helix-core"
255267version = "0.2.0"
256268dependencies = [
269+ "arc-swap",
257270 "etcetera",
258271 "helix-syntax",
259272 "once_cell",
@@ -354,6 +367,7 @@ dependencies = [
354367 "tokio",
355368 "toml",
356369 "url",
370+ "which",
357371]
358372
359373[[package]]
@@ -1057,6 +1071,16 @@ version = "0.10.2+wasi-snapshot-preview1"
10571071source = "registry+https://github.com/rust-lang/crates.io-index"
10581072checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
10591073
1074+[[package]]
1075+name = "which"
1076+version = "4.1.0"
1077+source = "registry+https://github.com/rust-lang/crates.io-index"
1078+checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
1079+dependencies = [
1080+ "either",
1081+ "libc",
1082+]
1083+
10601084[[package]]
10611085name = "winapi"
10621086version = "0.3.9"
book/src/SUMMARY.md+1
@@ -3,6 +3,7 @@
33- [Installation](./install.md)
44- [Usage](./usage.md)
55- [Configuration](./configuration.md)
6+ - [Themes](./themes.md)
67 - [Keymap](./keymap.md)
78 - [Key Remapping](./remapping.md)
89 - [Hooks](./hooks.md)
book/src/configuration.md+2−89
@@ -1,97 +1,10 @@
11# Configuration
22
3+To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
4+
35## LSP
46
57To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
68```toml
79lsp-progress = false
810```
9−
10−## Theme
11−
12−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).
13−
14−Styles in theme.toml are specified of in the form:
15−
16−```toml
17−key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
18−```
19−
20−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.
21−
22−To specify only the foreground color:
23−
24−```toml
25−key = "#ffffff"
26−```
27−
28−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).
29−
30−```toml
31−"key.key" = "#ffffff"
32−```
33−
34−Possible modifiers:
35−
36−| Modifier |
37−| --- |
38−| `bold` |
39−| `dim` |
40−| `italic` |
41−| `underlined` |
42−| `slow_blink` |
43−| `rapid_blink` |
44−| `reversed` |
45−| `hidden` |
46−| `crossed_out` |
47−
48−Possible keys:
49−
50−| Key | Notes |
51−| --- | --- |
52−| `attribute` | |
53−| `keyword` | |
54−| `keyword.directive` | Preprocessor directives (\#if in C) |
55−| `namespace` | |
56−| `punctuation` | |
57−| `punctuation.delimiter` | |
58−| `operator` | |
59−| `special` | |
60−| `property` | |
61−| `variable` | |
62−| `variable.parameter` | |
63−| `type` | |
64−| `type.builtin` | |
65−| `constructor` | |
66−| `function` | |
67−| `function.macro` | |
68−| `function.builtin` | |
69−| `comment` | |
70−| `variable.builtin` | |
71−| `constant` | |
72−| `constant.builtin` | |
73−| `string` | |
74−| `number` | |
75−| `escape` | Escaped characters |
76−| `label` | For lifetimes |
77−| `module` | |
78−| `ui.background` | |
79−| `ui.linenr` | |
80−| `ui.linenr.selected` | For lines with cursors |
81−| `ui.statusline` | |
82−| `ui.popup` | |
83−| `ui.window` | |
84−| `ui.help` | |
85−| `ui.text` | |
86−| `ui.text.focus` | |
87−| `ui.menu.selected` | |
88−| `ui.selection` | For selections in the editing area |
89−| `warning` | LSP warning |
90−| `error` | LSP error |
91−| `info` | LSP info |
92−| `hint` | LSP hint |
93−
94−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.
95−
96−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.
97−
book/src/keymap.md+12−8
@@ -69,9 +69,8 @@
6969| `;` | Collapse selection onto a single cursor |
7070| `Alt-;` | Flip selection cursor and anchor |
7171| `%` | Select entire file |
72−| `x` | Select current line |
73−| `X` | Extend to next line |
74−| `[` | Expand selection to parent syntax node TODO: pick a key |
72+| `x` | Select current line, if already selected, extend to next line |
73+| `` | Expand selection to parent syntax node TODO: pick a key |
7574| `J` | join lines inside selection |
7675| `K` | keep selections matching the regex TODO: overlapped by hover help |
7776| `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.
155154
156155| Key | Description |
157156| ----- | ------------- |
158−| `w`, `ctrl-w` | Switch to next window |
159−| `v`, `ctrl-v` | Vertical right split |
160−| `h`, `ctrl-h` | Horizontal bottom split |
161−| `q`, `ctrl-q` | Close current window |
157+| `w`, `Ctrl-w` | Switch to next window |
158+| `v`, `Ctrl-v` | Vertical right split |
159+| `h`, `Ctrl-h` | Horizontal bottom split |
160+| `q`, `Ctrl-q` | Close current window |
162161
163162## Space mode
164163
@@ -171,6 +170,11 @@ This layer is a kludge of mappings I had under leader key in neovim.
171170| `s` | Open symbol picker (current document) |
172171| `w` | Enter [window mode](#window-mode) |
173172| `space` | Keep primary selection TODO: it's here because space mode replaced it |
173+| `p` | paste system clipboard after selections |
174+| `P` | paste system clipboard before selections |
175+| `y` | join and yank selections to clipboard |
176+| `Y` | yank main selection to clipboard |
177+| `R` | replace selections by clipboard contents |
174178
175179# Picker
176180
@@ -184,4 +188,4 @@ Keys to use within picker.
184188| `Enter` | Open selected |
185189| `Ctrl-h` | Open horizontally |
186190| `Ctrl-v` | Open vertically |
187−| `Escape`, `ctrl-c` | Close picker |
191+| `Escape`, `Ctrl-c` | Close picker |
book/src/remapping.md+25−23
@@ -22,27 +22,29 @@ A-x = "normal_mode" # Maps Alt-X to enter normal mode
2222Control, Shift and Alt modifiers are encoded respectively with the prefixes
2323`C-`, `S-` and `A-`. Special keys are encoded as follows:
2424
25−* Backspace => "backspace"
26−* Space => "space"
27−* Return/Enter => "ret"
28−* < => "lt"
29−* \> => "gt"
30−* \+ => "plus"
31−* \- => "minus"
32−* ; => "semicolon"
33−* % => "percent"
34−* Left => "left"
35−* Right => "right"
36−* Up => "up"
37−* Home => "home"
38−* End => "end"
39−* Page Up => "pageup"
40−* Page Down => "pagedown"
41−* Tab => "tab"
42−* Back Tab => "backtab"
43−* Delete => "del"
44−* Insert => "ins"
45−* Null => "null"
46−* Escape => "esc"
25+| Key name | Representation |
26+| --- | --- |
27+| Backspace | `"backspace"` |
28+| Space | `"space"` |
29+| Return/Enter | `"ret"` |
30+| < | `"lt"` |
31+| \> | `"gt"` |
32+| \+ | `"plus"` |
33+| \- | `"minus"` |
34+| ; | `"semicolon"` |
35+| % | `"percent"` |
36+| Left | `"left"` |
37+| Right | `"right"` |
38+| Up | `"up"` |
39+| Home | `"home"` |
40+| End | `"end"` |
41+| Page | `"pageup"` |
42+| Page | `"pagedown"` |
43+| Tab | `"tab"` |
44+| Back | `"backtab"` |
45+| Delete | `"del"` |
46+| Insert | `"ins"` |
47+| Null | `"null"` |
48+| Escape | `"esc"` |
4749
48−Commands can be found in the source code at `../../helix-term/src/commands.rs`
50+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)
book/src/themes.mdnew file+94
@@ -0,0 +1,94 @@
1+# Themes
2+
3+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.
4+
5+To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
6+
7+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).
8+
9+## Creating a theme
10+
11+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`).
12+
13+Each line in the theme file is specified as below:
14+
15+```toml
16+key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
17+```
18+
19+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.
20+
21+To specify only the foreground color:
22+
23+```toml
24+key = "#ffffff"
25+```
26+
27+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).
28+
29+```toml
30+"key.key" = "#ffffff"
31+```
32+
33+Possible modifiers:
34+
35+| Modifier |
36+| --- |
37+| `bold` |
38+| `dim` |
39+| `italic` |
40+| `underlined` |
41+| `slow\_blink` |
42+| `rapid\_blink` |
43+| `reversed` |
44+| `hidden` |
45+| `crossed\_out` |
46+
47+Possible keys:
48+
49+| Key | Notes |
50+| --- | --- |
51+| `attribute` | |
52+| `keyword` | |
53+| `keyword.directive` | Preprocessor directives (\#if in C) |
54+| `namespace` | |
55+| `punctuation` | |
56+| `punctuation.delimiter` | |
57+| `operator` | |
58+| `special` | |
59+| `property` | |
60+| `variable` | |
61+| `variable.parameter` | |
62+| `type` | |
63+| `type.builtin` | |
64+| `constructor` | |
65+| `function` | |
66+| `function.macro` | |
67+| `function.builtin` | |
68+| `comment` | |
69+| `variable.builtin` | |
70+| `constant` | |
71+| `constant.builtin` | |
72+| `string` | |
73+| `number` | |
74+| `escape` | Escaped characters |
75+| `label` | For lifetimes |
76+| `module` | |
77+| `ui.background` | |
78+| `ui.linenr` | |
79+| `ui.statusline` | |
80+| `ui.popup` | |
81+| `ui.window` | |
82+| `ui.help` | |
83+| `ui.text` | |
84+| `ui.text.focus` | |
85+| `ui.menu.selected` | |
86+| `ui.selection` | For selections in the editing area |
87+| `warning` | LSP warning |
88+| `error` | LSP error |
89+| `info` | LSP info |
90+| `hint` | LSP hint |
91+
92+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.
93+
94+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.
contrib/themesnew file+1
@@ -0,0 +1 @@
1+../runtime/themes
helix-core/Cargo.toml+3−2
@@ -19,12 +19,13 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
1919ropey = "1.3"
2020smallvec = "1.4"
2121tendril = "0.4.2"
22−unicode-segmentation = "1.7.1"
22+unicode-segmentation = "1.7"
2323unicode-width = "0.1"
24−unicode-general-category = "0.4.0"
24+unicode-general-category = "0.4"
2525# slab = "0.4.2"
2626tree-sitter = "0.19"
2727once_cell = "1.8"
28+arc-swap = "1"
2829regex = "1"
2930
3031serde = { version = "1.0", features = ["derive"] }
helix-core/src/indent.rs+17−20
@@ -254,26 +254,23 @@ where
254254 Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
255255 };
256256 use once_cell::sync::OnceCell;
257− let loader = Loader::new(
258− Configuration {
259− language: vec![LanguageConfiguration {
260− scope: "source.rust".to_string(),
261− file_types: vec!["rs".to_string()],
262− language_id: Lang::Rust,
263− highlight_config: OnceCell::new(),
264− //
265− roots: vec![],
266− auto_format: false,
267− language_server: None,
268− indent: Some(IndentationConfiguration {
269− tab_width: 4,
270− unit: String::from(" "),
271− }),
272− indent_query: OnceCell::new(),
273− }],
274− },
275− Vec::new(),
276− );
257+ let loader = Loader::new(Configuration {
258+ language: vec![LanguageConfiguration {
259+ scope: "source.rust".to_string(),
260+ file_types: vec!["rs".to_string()],
261+ language_id: Lang::Rust,
262+ highlight_config: OnceCell::new(),
263+ //
264+ roots: vec![],
265+ auto_format: false,
266+ language_server: None,
267+ indent: Some(IndentationConfiguration {
268+ tab_width: 4,
269+ unit: String::from(" "),
270+ }),
271+ indent_query: OnceCell::new(),
272+ }],
273+ });
277274
278275 // set runtime path so we can find the queries
279276 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
helix-core/src/lib.rs+7−3
@@ -19,6 +19,12 @@ mod state;
1919pub mod syntax;
2020mod transaction;
2121
22+pub mod unicode {
23+ pub use unicode_general_category as category;
24+ pub use unicode_segmentation as segmentation;
25+ pub use unicode_width as width;
26+}
27+
2228static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
2329 once_cell::sync::Lazy::new(runtime_dir);
2430
@@ -51,7 +57,7 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
5157}
5258
5359#[cfg(not(embed_runtime))]
54−fn runtime_dir() -> std::path::PathBuf {
60+pub fn runtime_dir() -> std::path::PathBuf {
5561 if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
5662 return dir.into();
5763 }
@@ -98,8 +104,6 @@ pub use ropey::{Rope, RopeSlice};
98104
99105pub use tendril::StrTendril as Tendril;
100106
101−pub use unicode_general_category::get_general_category;
102−
103107#[doc(inline)]
104108pub use {regex, tree_sitter};
105109
helix-core/src/syntax.rs+57−41
@@ -1,6 +1,8 @@
11use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
22pub use helix_syntax::{get_language, get_language_name, Lang};
33
4+use arc_swap::ArcSwap;
5+
46use std::{
57 borrow::Cow,
68 cell::RefCell,
@@ -143,37 +145,49 @@ fn read_query(language: &str, filename: &str) -> String {
143145}
144146
145147impl LanguageConfiguration {
146− pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
147− self.highlight_config
148− .get_or_init(|| {
149− let language = get_language_name(self.language_id).to_ascii_lowercase();
148+ fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
149+ let language = get_language_name(self.language_id).to_ascii_lowercase();
150150
151− let highlights_query = read_query(&language, "highlights.scm");
152− // always highlight syntax errors
153− // highlights_query += "\n(ERROR) @error";
151+ let highlights_query = read_query(&language, "highlights.scm");
152+ // always highlight syntax errors
153+ // highlights_query += "\n(ERROR) @error";
154154
155− let injections_query = read_query(&language, "injections.scm");
155+ let injections_query = read_query(&language, "injections.scm");
156156
157− let locals_query = "";
157+ let locals_query = "";
158158
159− if highlights_query.is_empty() {
160− None
161− } else {
162− let language = get_language(self.language_id);
163− let mut config = HighlightConfiguration::new(
164− language,
165− &highlights_query,
166− &injections_query,
167− locals_query,
168− )
169− .unwrap(); // TODO: no unwrap
170− config.configure(scopes);
171− Some(Arc::new(config))
172− }
173− })
159+ if highlights_query.is_empty() {
160+ None
161+ } else {
162+ let language = get_language(self.language_id);
163+ let mut config = HighlightConfiguration::new(
164+ language,
165+ &highlights_query,
166+ &injections_query,
167+ locals_query,
168+ )
169+ .unwrap(); // TODO: no unwrap
170+ config.configure(scopes);
171+ Some(Arc::new(config))
172+ }
173+ }
174+
175+ pub fn reconfigure(&self, scopes: &[String]) {
176+ if let Some(Some(config)) = self.highlight_config.get() {
177+ config.configure(scopes);
178+ }
179+ }
180+
181+ pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
182+ self.highlight_config
183+ .get_or_init(|| self.initialize_highlight(scopes))
174184 .clone()
175185 }
176186
187+ pub fn is_highlight_initialized(&self) -> bool {
188+ self.highlight_config.get().is_some()
189+ }
190+
177191 pub fn indent_query(&self) -> Option<&IndentQuery> {
178192 self.indent_query
179193 .get_or_init(|| {
@@ -190,22 +204,18 @@ impl LanguageConfiguration {
190204 }
191205}
192206
193−pub static LOADER: OnceCell<Loader> = OnceCell::new();
194−
195207#[derive(Debug)]
196208pub struct Loader {
197209 // highlight_names ?
198210 language_configs: Vec<Arc<LanguageConfiguration>>,
199211 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
200− scopes: Vec<String>,
201212}
202213
203214impl Loader {
204− pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
215+ pub fn new(config: Configuration) -> Self {
205216 let mut loader = Self {
206217 language_configs: Vec::new(),
207218 language_config_ids_by_file_type: HashMap::new(),
208− scopes,
209219 };
210220
211221 for config in config.language {
@@ -225,10 +235,6 @@ impl Loader {
225235 loader
226236 }
227237
228− pub fn scopes(&self) -> &[String] {
229− &self.scopes
230− }
231−
232238 pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
233239 // Find all the language configurations that match this file name
234240 // or a suffix of the file name.
@@ -253,6 +259,10 @@ impl Loader {
253259 .find(|config| config.scope == scope)
254260 .cloned()
255261 }
262+
263+ pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
264+ self.language_configs.iter()
265+ }
256266}
257267
258268pub struct TsParser {
@@ -772,7 +782,7 @@ pub struct HighlightConfiguration {
772782 combined_injections_query: Option<Query>,
773783 locals_pattern_index: usize,
774784 highlights_pattern_index: usize,
775− highlight_indices: Vec<Option<Highlight>>,
785+ highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
776786 non_local_variable_patterns: Vec<bool>,
777787 injection_content_capture_index: Option<u32>,
778788 injection_language_capture_index: Option<u32>,
@@ -924,7 +934,7 @@ impl HighlightConfiguration {
924934 }
925935 }
926936
927− let highlight_indices = vec![None; query.capture_names().len()];
937+ let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
928938 Ok(Self {
929939 language,
930940 query,
@@ -957,17 +967,20 @@ impl HighlightConfiguration {
957967 ///
958968 /// When highlighting, results are returned as `Highlight` values, which contain the index
959969 /// of the matched highlight this list of highlight names.
960− pub fn configure(&mut self, recognized_names: &[String]) {
970+ pub fn configure(&self, recognized_names: &[String]) {
961971 let mut capture_parts = Vec::new();
962− self.highlight_indices.clear();
963− self.highlight_indices
964− .extend(self.query.capture_names().iter().map(move |capture_name| {
972+ let indices: Vec<_> = self
973+ .query
974+ .capture_names()
975+ .iter()
976+ .map(move |capture_name| {
965977 capture_parts.clear();
966978 capture_parts.extend(capture_name.split('.'));
967979
968980 let mut best_index = None;
969981 let mut best_match_len = 0;
970982 for (i, recognized_name) in recognized_names.iter().enumerate() {
983+ let recognized_name = recognized_name;
971984 let mut len = 0;
972985 let mut matches = true;
973986 for part in recognized_name.split('.') {
@@ -983,7 +996,10 @@ impl HighlightConfiguration {
983996 }
984997 }
985998 best_index.map(Highlight)
986− }));
999+ })
1000+ .collect();
1001+
1002+ self.highlight_indices.store(Arc::new(indices));
9871003 }
9881004}
9891005
@@ -1562,7 +1578,7 @@ where
15621578 }
15631579 }
15641580
1565− let current_highlight = layer.config.highlight_indices[capture.index as usize];
1581+ let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
15661582
15671583 // If this node represents a local definition, then store the current
15681584 // highlight value on the local scope entry representing this node.
helix-term/src/application.rs+39−5
@@ -1,7 +1,8 @@
1+use helix_core::syntax;
12use helix_lsp::{lsp, LspProgressMap};
2−use helix_view::{document::Mode, Document, Editor, Theme, View};
3+use helix_view::{document::Mode, theme, Document, Editor, Theme, View};
34
4−use crate::{args::Args, compositor::Compositor, config::Config, ui};
5+use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
56
67use log::{error, info};
78
@@ -14,7 +15,7 @@ use std::{
1415 time::Duration,
1516};
1617
17−use anyhow::Error;
18+use anyhow::{Context, Error};
1819
1920use crossterm::{
2021 event::{Event, EventStream},
@@ -36,6 +37,8 @@ pub struct Application {
3637 compositor: Compositor,
3738 editor: Editor,
3839
40+ theme_loader: Arc<theme::Loader>,
41+ syn_loader: Arc<syntax::Loader>,
3942 callbacks: LspCallbacks,
4043
4144 lsp_progress: LspProgressMap,
@@ -47,9 +50,36 @@ impl Application {
4750 use helix_view::editor::Action;
4851 let mut compositor = Compositor::new()?;
4952 let size = compositor.size();
50− let mut editor = Editor::new(size);
5153
52− let mut editor_view = Box::new(ui::EditorView::new(config.keys));
54+ let conf_dir = helix_core::config_dir();
55+
56+ let theme_loader =
57+ std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
58+
59+ // load $HOME/.config/helix/languages.toml, fallback to default config
60+ let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
61+ let lang_conf = lang_conf
62+ .as_deref()
63+ .unwrap_or(include_bytes!("../../languages.toml"));
64+
65+ let theme = if let Some(theme) = &config.global.theme {
66+ match theme_loader.load(theme) {
67+ Ok(theme) => theme,
68+ Err(e) => {
69+ log::warn!("failed to load theme `{}` - {}", theme, e);
70+ theme_loader.default()
71+ }
72+ }
73+ } else {
74+ theme_loader.default()
75+ };
76+
77+ let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
78+ let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
79+
80+ let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone());
81+
82+ let mut editor_view = Box::new(ui::EditorView::new(config.keymaps));
5383 compositor.push(editor_view);
5484
5585 if !args.files.is_empty() {
@@ -72,10 +102,14 @@ impl Application {
72102 editor.new_file(Action::VerticalSplit);
73103 }
74104
105+ editor.set_theme(theme);
106+
75107 let mut app = Self {
76108 compositor,
77109 editor,
78110
111+ theme_loader,
112+ syn_loader,
79113 callbacks: FuturesUnordered::new(),
80114 lsp_progress: LspProgressMap::new(),
81115 lsp_progress_enabled: config.global.lsp_progress,
helix-term/src/commands.rs+232−62
@@ -11,7 +11,6 @@ use helix_core::{
1111
1212use helix_view::{
1313 document::{IndentStyle, Mode},
14− input::{KeyCode, KeyEvent},
1514 view::{View, PADDING},
1615 Document, DocumentId, Editor, ViewId,
1716};
@@ -39,8 +38,8 @@ use std::{
3938 path::{Path, PathBuf},
4039};
4140
41+use crossterm::event::{KeyCode, KeyEvent};
4242use once_cell::sync::Lazy;
43−use serde::de::{self, Deserialize, Deserializer};
4443
4544pub struct Context<'a> {
4645 pub selected_register: helix_view::RegisterSelection,
@@ -186,7 +185,6 @@ impl Command {
186185 search_next,
187186 extend_search_next,
188187 search_selection,
189− select_line,
190188 extend_line,
191189 delete_selection,
192190 change_selection,
@@ -223,9 +221,14 @@ impl Command {
223221 undo,
224222 redo,
225223 yank,
224+ yank_joined_to_clipboard,
225+ yank_main_selection_to_clipboard,
226226 replace_with_yanked,
227+ replace_selections_with_clipboard,
227228 paste_after,
228229 paste_before,
230+ paste_clipboard_after,
231+ paste_clipboard_before,
229232 indent,
230233 unindent,
231234 format_selections,
@@ -253,48 +256,6 @@ impl Command {
253256 );
254257}
255258
256−impl fmt::Debug for Command {
257− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258− let Command(name, _) = self;
259− f.debug_tuple("Command").field(name).finish()
260− }
261−}
262−
263−impl fmt::Display for Command {
264− fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265− let Command(name, _) = self;
266− f.write_str(name)
267− }
268−}
269−
270−impl std::str::FromStr for Command {
271− type Err = anyhow::Error;
272−
273− fn from_str(s: &str) -> Result<Self, Self::Err> {
274− Command::COMMAND_LIST
275− .iter()
276− .copied()
277− .find(|cmd| cmd.0 == s)
278− .ok_or_else(|| anyhow!("No command named '{}'", s))
279− }
280−}
281−
282−impl<'de> Deserialize<'de> for Command {
283− fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284− where
285− D: Deserializer<'de>,
286− {
287− let s = String::deserialize(deserializer)?;
288− s.parse().map_err(de::Error::custom)
289− }
290−}
291−
292−impl PartialEq for Command {
293− fn eq(&self, other: &Self) -> bool {
294− self.name() == other.name()
295− }
296−}
297−
298259fn move_char_left(cx: &mut Context) {
299260 let count = cx.count();
300261 let (view, doc) = current!(cx.editor);
@@ -926,21 +887,6 @@ fn search_selection(cx: &mut Context) {
926887
927888//
928889
929−fn select_line(cx: &mut Context) {
930− let count = cx.count();
931− let (view, doc) = current!(cx.editor);
932−
933− let pos = doc.selection(view.id).primary();
934− let text = doc.text();
935−
936− let line = text.char_to_line(pos.head);
937− let start = text.line_to_char(line);
938− let end = text
939− .line_to_char(std::cmp::min(doc.text().len_lines(), line + count))
940− .saturating_sub(1);
941−
942− doc.set_selection(view.id, Selection::single(start, end));
943−}
944890fn extend_line(cx: &mut Context) {
945891 let count = cx.count();
946892 let (view, doc) = current!(cx.editor);
@@ -1318,6 +1264,57 @@ mod cmd {
13181264 quit_all_impl(editor, args, event, true)
13191265 }
13201266
1267+ fn theme(editor: &mut Editor, args: &[&str], event: PromptEvent) {
1268+ let theme = if let Some(theme) = args.first() {
1269+ theme
1270+ } else {
1271+ editor.set_error("theme name not provided".into());
1272+ return;
1273+ };
1274+
1275+ editor.set_theme_from_name(theme);
1276+ }
1277+
1278+ fn yank_main_selection_to_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
1279+ yank_main_selection_to_clipboard_impl(editor);
1280+ }
1281+
1282+ fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], _: PromptEvent) {
1283+ let separator = args.first().copied().unwrap_or("\n");
1284+ yank_joined_to_clipboard_impl(editor, separator);
1285+ }
1286+
1287+ fn paste_clipboard_after(editor: &mut Editor, _: &[&str], _: PromptEvent) {
1288+ paste_clipboard_impl(editor, Paste::After);
1289+ }
1290+
1291+ fn paste_clipboard_before(editor: &mut Editor, _: &[&str], _: PromptEvent) {
1292+ paste_clipboard_impl(editor, Paste::After);
1293+ }
1294+
1295+ fn replace_selections_with_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
1296+ let (view, doc) = current!(editor);
1297+
1298+ match editor.clipboard_provider.get_contents() {
1299+ Ok(contents) => {
1300+ let transaction =
1301+ Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
1302+ let max_to = doc.text().len_chars().saturating_sub(1);
1303+ let to = std::cmp::min(max_to, range.to() + 1);
1304+ (range.from(), to, Some(contents.as_str().into()))
1305+ });
1306+
1307+ doc.apply(&transaction, view.id);
1308+ doc.append_changes_to_history(view.id);
1309+ }
1310+ Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
1311+ }
1312+ }
1313+
1314+ fn show_clipboard_provider(editor: &mut Editor, _: &[&str], _: PromptEvent) {
1315+ editor.set_status(editor.clipboard_provider.name().into());
1316+ }
1317+
13211318 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
13221319 TypableCommand {
13231320 name: "quit",
@@ -1431,7 +1428,55 @@ mod cmd {
14311428 fun: force_quit_all,
14321429 completer: None,
14331430 },
1434−
1431+ TypableCommand {
1432+ name: "theme",
1433+ alias: None,
1434+ doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
1435+ fun: theme,
1436+ completer: Some(completers::theme),
1437+ },
1438+ TypableCommand {
1439+ name: "clipboard-yank",
1440+ alias: None,
1441+ doc: "Yank main selection into system clipboard.",
1442+ fun: yank_main_selection_to_clipboard,
1443+ completer: None,
1444+ },
1445+ TypableCommand {
1446+ name: "clipboard-yank-join",
1447+ alias: None,
1448+ 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.
1449+ fun: yank_joined_to_clipboard,
1450+ completer: None,
1451+ },
1452+ TypableCommand {
1453+ name: "clipboard-paste-after",
1454+ alias: None,
1455+ doc: "Paste system clipboard after selections.",
1456+ fun: paste_clipboard_after,
1457+ completer: None,
1458+ },
1459+ TypableCommand {
1460+ name: "clipboard-paste-before",
1461+ alias: None,
1462+ doc: "Paste system clipboard before selections.",
1463+ fun: paste_clipboard_before,
1464+ completer: None,
1465+ },
1466+ TypableCommand {
1467+ name: "clipboard-paste-replace",
1468+ alias: None,
1469+ doc: "Replace selections with content of system clipboard.",
1470+ fun: replace_selections_with_clipboard,
1471+ completer: None,
1472+ },
1473+ TypableCommand {
1474+ name: "show-clipboard-provider",
1475+ alias: None,
1476+ doc: "Show clipboard provider name in status bar.",
1477+ fun: show_clipboard_provider,
1478+ completer: None,
1479+ },
14351480 ];
14361481
14371482 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2424,6 +2469,52 @@ fn yank(cx: &mut Context) {
24242469 cx.editor.set_status(msg)
24252470}
24262471
2472+fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) {
2473+ let (view, doc) = current!(editor);
2474+
2475+ let values: Vec<String> = doc
2476+ .selection(view.id)
2477+ .fragments(doc.text().slice(..))
2478+ .map(Cow::into_owned)
2479+ .collect();
2480+
2481+ let msg = format!(
2482+ "joined and yanked {} selection(s) to system clipboard",
2483+ values.len(),
2484+ );
2485+
2486+ let joined = values.join(separator);
2487+
2488+ if let Err(e) = editor.clipboard_provider.set_contents(joined) {
2489+ log::error!("Couldn't set system clipboard content: {:?}", e);
2490+ }
2491+
2492+ editor.set_status(msg);
2493+}
2494+
2495+fn yank_joined_to_clipboard(cx: &mut Context) {
2496+ yank_joined_to_clipboard_impl(&mut cx.editor, "\n");
2497+}
2498+
2499+fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) {
2500+ let (view, doc) = current!(editor);
2501+
2502+ let value = doc
2503+ .selection(view.id)
2504+ .primary()
2505+ .fragment(doc.text().slice(..));
2506+
2507+ if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
2508+ log::error!("Couldn't set system clipboard content: {:?}", e);
2509+ }
2510+
2511+ editor.set_status("yanked main selection to system clipboard".to_owned());
2512+}
2513+
2514+fn yank_main_selection_to_clipboard(cx: &mut Context) {
2515+ yank_main_selection_to_clipboard_impl(&mut cx.editor);
2516+}
2517+
24272518#[derive(Copy, Clone)]
24282519enum Paste {
24292520 Before,
@@ -2469,6 +2560,31 @@ fn paste_impl(
24692560 Some(transaction)
24702561}
24712562
2563+fn paste_clipboard_impl(editor: &mut Editor, action: Paste) {
2564+ let (view, doc) = current!(editor);
2565+
2566+ match editor
2567+ .clipboard_provider
2568+ .get_contents()
2569+ .map(|contents| paste_impl(&[contents], doc, view, action))
2570+ {
2571+ Ok(Some(transaction)) => {
2572+ doc.apply(&transaction, view.id);
2573+ doc.append_changes_to_history(view.id);
2574+ }
2575+ Ok(None) => {}
2576+ Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
2577+ }
2578+}
2579+
2580+fn paste_clipboard_after(cx: &mut Context) {
2581+ paste_clipboard_impl(&mut cx.editor, Paste::After);
2582+}
2583+
2584+fn paste_clipboard_before(cx: &mut Context) {
2585+ paste_clipboard_impl(&mut cx.editor, Paste::Before);
2586+}
2587+
24722588fn replace_with_yanked(cx: &mut Context) {
24732589 let reg_name = cx.selected_register.name();
24742590 let (view, doc) = current!(cx.editor);
@@ -2489,6 +2605,29 @@ fn replace_with_yanked(cx: &mut Context) {
24892605 }
24902606}
24912607
2608+fn replace_selections_with_clipboard_impl(editor: &mut Editor) {
2609+ let (view, doc) = current!(editor);
2610+
2611+ match editor.clipboard_provider.get_contents() {
2612+ Ok(contents) => {
2613+ let transaction =
2614+ Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
2615+ let max_to = doc.text().len_chars().saturating_sub(1);
2616+ let to = std::cmp::min(max_to, range.to() + 1);
2617+ (range.from(), to, Some(contents.as_str().into()))
2618+ });
2619+
2620+ doc.apply(&transaction, view.id);
2621+ doc.append_changes_to_history(view.id);
2622+ }
2623+ Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
2624+ }
2625+}
2626+
2627+fn replace_selections_with_clipboard(cx: &mut Context) {
2628+ replace_selections_with_clipboard_impl(&mut cx.editor);
2629+}
2630+
24922631// alt-p => paste every yanked selection after selected text
24932632// alt-P => paste every yanked selection before selected text
24942633// R => replace selected text with yanked text
@@ -2854,7 +2993,7 @@ fn hover(cx: &mut Context) {
28542993
28552994 // skip if contents empty
28562995
2857− let contents = ui::Markdown::new(contents);
2996+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
28582997 let mut popup = Popup::new(contents);
28592998 compositor.push(Box::new(popup));
28602999 }
@@ -3009,6 +3148,11 @@ fn space_mode(cx: &mut Context) {
30093148 'b' => buffer_picker(cx),
30103149 's' => symbol_picker(cx),
30113150 'w' => window_mode(cx),
3151+ 'y' => yank_joined_to_clipboard(cx),
3152+ 'Y' => yank_main_selection_to_clipboard(cx),
3153+ 'p' => paste_clipboard_after(cx),
3154+ 'P' => paste_clipboard_before(cx),
3155+ 'R' => replace_selections_with_clipboard(cx),
30123156 // ' ' => toggle_alternate_buffer(cx),
30133157 // TODO: temporary since space mode took its old key
30143158 ' ' => keep_primary_selection(cx),
@@ -3092,3 +3236,29 @@ fn right_bracket_mode(cx: &mut Context) {
30923236 }
30933237 })
30943238}
3239+
3240+impl fmt::Display for Command {
3241+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3242+ let Command(name, _) = self;
3243+ f.write_str(name)
3244+ }
3245+}
3246+
3247+impl std::str::FromStr for Command {
3248+ type Err = anyhow::Error;
3249+
3250+ fn from_str(s: &str) -> Result<Self, Self::Err> {
3251+ Command::COMMAND_LIST
3252+ .iter()
3253+ .copied()
3254+ .find(|cmd| cmd.0 == s)
3255+ .ok_or_else(|| anyhow!("No command named '{}'", s))
3256+ }
3257+}
3258+
3259+impl fmt::Debug for Command {
3260+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3261+ let Command(name, _) = self;
3262+ f.debug_tuple("Command").field(name).finish()
3263+ }
3264+}
helix-term/src/compositor.rs+7−7
@@ -178,13 +178,13 @@ pub trait AnyComponent {
178178 /// Returns a boxed any from a boxed self.
179179 ///
180180 /// Can be used before `Box::downcast()`.
181− ///
182− /// # Examples
183− ///
184− /// ```rust
185− /// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
186− /// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
187− /// ```
181+ //
182+ // # Examples
183+ //
184+ // ```rust
185+ // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
186+ // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
187+ // ```
188188 fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
189189}
190190
helix-term/src/config.rs+38−46
@@ -1,63 +1,55 @@
1−use serde::Deserialize;
1+use anyhow::{Error, Result};
2+use std::{collections::HashMap, str::FromStr};
23
3−use crate::commands::Command;
4−use crate::keymap::Keymaps;
4+use serde::{de::Error as SerdeError, Deserialize, Serialize};
5+
6+use crate::keymap::{parse_keymaps, Keymaps};
57
6−#[derive(Debug, PartialEq, Deserialize)]
78pub struct GlobalConfig {
9+ pub theme: Option<String>,
810 pub lsp_progress: bool,
911}
1012
1113impl Default for GlobalConfig {
1214 fn default() -> Self {
13− Self { lsp_progress: true }
15+ Self {
16+ lsp_progress: true,
17+ theme: None,
18+ }
1419 }
1520}
1621
17−#[derive(Debug, Default, PartialEq, Deserialize)]
18−#[serde(default)]
22+#[derive(Default)]
1923pub struct Config {
2024 pub global: GlobalConfig,
21− pub keys: Keymaps,
25+ pub keymaps: Keymaps,
2226}
2327
24−#[test]
25−fn parsing_keymaps_config_file() {
26− use helix_core::hashmap;
27− use helix_view::document::Mode;
28− use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
29−
30− let sample_keymaps = r#"
31− [keys.insert]
32− y = "move_line_down"
33− S-C-a = "delete_selection"
34−
35− [keys.normal]
36− A-F12 = "move_next_word_end"
37− "#;
28+#[derive(Serialize, Deserialize)]
29+#[serde(rename_all = "kebab-case")]
30+struct TomlConfig {
31+ theme: Option<String>,
32+ lsp_progress: Option<bool>,
33+ keys: Option<HashMap<String, HashMap<String, String>>>,
34+}
3835
39− assert_eq!(
40− toml::from_str::<Config>(sample_keymaps).unwrap(),
41− Config {
42− global: Default::default(),
43− keys: Keymaps(hashmap! {
44− Mode::Insert => hashmap! {
45− KeyEvent {
46− code: KeyCode::Char('y'),
47− modifiers: KeyModifiers::NONE,
48− } => Command::move_line_down,
49− KeyEvent {
50− code: KeyCode::Char('a'),
51− modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
52− } => Command::delete_selection,
53− },
54− Mode::Normal => hashmap! {
55− KeyEvent {
56− code: KeyCode::F(12),
57− modifiers: KeyModifiers::ALT,
58− } => Command::move_next_word_end,
59− },
60− })
61− }
62− );
36+impl<'de> Deserialize<'de> for Config {
37+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38+ where
39+ D: serde::Deserializer<'de>,
40+ {
41+ let config = TomlConfig::deserialize(deserializer)?;
42+ Ok(Self {
43+ global: GlobalConfig {
44+ lsp_progress: config.lsp_progress.unwrap_or(true),
45+ theme: config.theme,
46+ },
47+ keymaps: config
48+ .keys
49+ .map(|r| parse_keymaps(&r))
50+ .transpose()
51+ .map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))?
52+ .unwrap_or_else(Keymaps::default),
53+ })
54+ }
6355}
helix-term/src/keymap.rs+319−23
@@ -3,8 +3,6 @@ pub use crate::commands::Command;
33use anyhow::{anyhow, Error, Result};
44use helix_core::hashmap;
55use helix_view::document::Mode;
6−use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
7−use serde::Deserialize;
86use std::{
97 collections::HashMap,
108 fmt::Display,
@@ -101,6 +99,14 @@ use std::{
10199// D] = last diagnostic
102100// }
103101
102+// #[cfg(feature = "term")]
103+pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
104+
105+#[derive(Clone, Debug)]
106+pub struct Keymap(pub HashMap<KeyEvent, Command>);
107+#[derive(Clone, Debug)]
108+pub struct Keymaps(pub HashMap<Mode, Keymap>);
109+
104110#[macro_export]
105111macro_rules! key {
106112 ($key:ident) => {
@@ -135,21 +141,9 @@ macro_rules! alt {
135141 };
136142}
137143
138−#[derive(Debug, PartialEq, Deserialize)]
139−#[serde(transparent)]
140−pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
141−
142−impl Deref for Keymaps {
143− type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
144−
145− fn deref(&self) -> &Self::Target {
146− &self.0
147− }
148−}
149−
150144impl Default for Keymaps {
151− fn default() -> Keymaps {
152− let normal = hashmap!(
145+ fn default() -> Self {
146+ let normal = Keymap(hashmap!(
153147 key!('h') => Command::move_char_left,
154148 key!('j') => Command::move_line_down,
155149 key!('k') => Command::move_line_up,
@@ -202,9 +196,7 @@ impl Default for Keymaps {
202196 key!(';') => Command::collapse_selection,
203197 alt!(';') => Command::flip_selections,
204198 key!('%') => Command::select_all,
205− key!('x') => Command::select_line,
206− key!('X') => Command::extend_line,
207− // or select mode X?
199+ key!('x') => Command::extend_line,
208200 // extend_to_whole_line, crop_to_whole_line
209201
210202
@@ -283,12 +275,12 @@ impl Default for Keymaps {
283275 key!('z') => Command::view_mode,
284276
285277 key!('"') => Command::select_register,
286− );
278+ ));
287279 // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
288280 // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
289281 // because some selection operations can now be done from normal mode, some from select mode.
290282 let mut select = normal.clone();
291− select.extend(
283+ select.0.extend(
292284 hashmap!(
293285 key!('h') => Command::extend_char_left,
294286 key!('j') => Command::extend_line_down,
@@ -321,7 +313,7 @@ impl Default for Keymaps {
321313 // TODO: select could be normal mode with some bindings merged over
322314 Mode::Normal => normal,
323315 Mode::Select => select,
324− Mode::Insert => hashmap!(
316+ Mode::Insert => Keymap(hashmap!(
325317 key!(Esc) => Command::normal_mode as Command,
326318 key!(Backspace) => Command::delete_char_backward,
327319 key!(Delete) => Command::delete_char_forward,
@@ -333,9 +325,313 @@ impl Default for Keymaps {
333325 key!(Right) => Command::move_char_right,
334326 key!(PageUp) => Command::page_up,
335327 key!(PageDown) => Command::page_down,
328+ key!(Home) => Command::move_line_start,
329+ key!(End) => Command::move_line_end,
336330 ctrl!('x') => Command::completion,
337331 ctrl!('w') => Command::delete_word_backward,
338− ),
332+ )),
339333 ))
340334 }
341335}
336+
337+// Newtype wrapper over keys to allow toml serialization/parsing
338+#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)]
339+pub struct RepresentableKeyEvent(pub KeyEvent);
340+impl Display for RepresentableKeyEvent {
341+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342+ let Self(key) = self;
343+ f.write_fmt(format_args!(
344+ "{}{}{}",
345+ if key.modifiers.contains(KeyModifiers::SHIFT) {
346+ "S-"
347+ } else {
348+ ""
349+ },
350+ if key.modifiers.contains(KeyModifiers::ALT) {
351+ "A-"
352+ } else {
353+ ""
354+ },
355+ if key.modifiers.contains(KeyModifiers::CONTROL) {
356+ "C-"
357+ } else {
358+ ""
359+ },
360+ ))?;
361+ match key.code {
362+ KeyCode::Backspace => f.write_str("backspace")?,
363+ KeyCode::Enter => f.write_str("ret")?,
364+ KeyCode::Left => f.write_str("left")?,
365+ KeyCode::Right => f.write_str("right")?,
366+ KeyCode::Up => f.write_str("up")?,
367+ KeyCode::Down => f.write_str("down")?,
368+ KeyCode::Home => f.write_str("home")?,
369+ KeyCode::End => f.write_str("end")?,
370+ KeyCode::PageUp => f.write_str("pageup")?,
371+ KeyCode::PageDown => f.write_str("pagedown")?,
372+ KeyCode::Tab => f.write_str("tab")?,
373+ KeyCode::BackTab => f.write_str("backtab")?,
374+ KeyCode::Delete => f.write_str("del")?,
375+ KeyCode::Insert => f.write_str("ins")?,
376+ KeyCode::Null => f.write_str("null")?,
377+ KeyCode::Esc => f.write_str("esc")?,
378+ KeyCode::Char('<') => f.write_str("lt")?,
379+ KeyCode::Char('>') => f.write_str("gt")?,
380+ KeyCode::Char('+') => f.write_str("plus")?,
381+ KeyCode::Char('-') => f.write_str("minus")?,
382+ KeyCode::Char(';') => f.write_str("semicolon")?,
383+ KeyCode::Char('%') => f.write_str("percent")?,
384+ KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
385+ KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
386+ };
387+ Ok(())
388+ }
389+}
390+
391+impl FromStr for RepresentableKeyEvent {
392+ type Err = Error;
393+
394+ fn from_str(s: &str) -> Result<Self, Self::Err> {
395+ let mut tokens: Vec<_> = s.split('-').collect();
396+ let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
397+ "backspace" => KeyCode::Backspace,
398+ "space" => KeyCode::Char(' '),
399+ "ret" => KeyCode::Enter,
400+ "lt" => KeyCode::Char('<'),
401+ "gt" => KeyCode::Char('>'),
402+ "plus" => KeyCode::Char('+'),
403+ "minus" => KeyCode::Char('-'),
404+ "semicolon" => KeyCode::Char(';'),
405+ "percent" => KeyCode::Char('%'),
406+ "left" => KeyCode::Left,
407+ "right" => KeyCode::Right,
408+ "up" => KeyCode::Down,
409+ "home" => KeyCode::Home,
410+ "end" => KeyCode::End,
411+ "pageup" => KeyCode::PageUp,
412+ "pagedown" => KeyCode::PageDown,
413+ "tab" => KeyCode::Tab,
414+ "backtab" => KeyCode::BackTab,
415+ "del" => KeyCode::Delete,
416+ "ins" => KeyCode::Insert,
417+ "null" => KeyCode::Null,
418+ "esc" => KeyCode::Esc,
419+ single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
420+ function if function.len() > 1 && function.starts_with('F') => {
421+ let function: String = function.chars().skip(1).collect();
422+ let function = str::parse::<u8>(&function)?;
423+ (function > 0 && function < 13)
424+ .then(|| KeyCode::F(function))
425+ .ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
426+ }
427+ invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
428+ };
429+
430+ let mut modifiers = KeyModifiers::empty();
431+ for token in tokens {
432+ let flag = match token {
433+ "S" => KeyModifiers::SHIFT,
434+ "A" => KeyModifiers::ALT,
435+ "C" => KeyModifiers::CONTROL,
436+ _ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
437+ };
438+
439+ if modifiers.contains(flag) {
440+ return Err(anyhow!("Repeated key modifier '{}-'", token));
441+ }
442+ modifiers.insert(flag);
443+ }
444+
445+ Ok(RepresentableKeyEvent(KeyEvent { code, modifiers }))
446+ }
447+}
448+
449+pub fn parse_keymaps(toml_keymaps: &HashMap<String, HashMap<String, String>>) -> Result<Keymaps> {
450+ let mut keymaps = Keymaps::default();
451+
452+ for (mode, map) in toml_keymaps {
453+ let mode = Mode::from_str(&mode)?;
454+ for (key, command) in map {
455+ let key = str::parse::<RepresentableKeyEvent>(&key)?;
456+ let command = str::parse::<Command>(&command)?;
457+ keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command);
458+ }
459+ }
460+ Ok(keymaps)
461+}
462+
463+impl Deref for Keymap {
464+ type Target = HashMap<KeyEvent, Command>;
465+ fn deref(&self) -> &Self::Target {
466+ &self.0
467+ }
468+}
469+
470+impl Deref for Keymaps {
471+ type Target = HashMap<Mode, Keymap>;
472+ fn deref(&self) -> &Self::Target {
473+ &self.0
474+ }
475+}
476+
477+impl DerefMut for Keymap {
478+ fn deref_mut(&mut self) -> &mut Self::Target {
479+ &mut self.0
480+ }
481+}
482+
483+impl DerefMut for Keymaps {
484+ fn deref_mut(&mut self) -> &mut Self::Target {
485+ &mut self.0
486+ }
487+}
488+
489+#[cfg(test)]
490+mod test {
491+ use crate::config::Config;
492+
493+ use super::*;
494+
495+ impl PartialEq for Command {
496+ fn eq(&self, other: &Self) -> bool {
497+ self.name() == other.name()
498+ }
499+ }
500+
501+ #[test]
502+ fn parsing_keymaps_config_file() {
503+ let sample_keymaps = r#"
504+ [keys.insert]
505+ y = "move_line_down"
506+ S-C-a = "delete_selection"
507+
508+ [keys.normal]
509+ A-F12 = "move_next_word_end"
510+ "#;
511+
512+ let config: Config = toml::from_str(sample_keymaps).unwrap();
513+ assert_eq!(
514+ *config
515+ .keymaps
516+ .0
517+ .get(&Mode::Insert)
518+ .unwrap()
519+ .0
520+ .get(&KeyEvent {
521+ code: KeyCode::Char('y'),
522+ modifiers: KeyModifiers::NONE
523+ })
524+ .unwrap(),
525+ Command::move_line_down
526+ );
527+ assert_eq!(
528+ *config
529+ .keymaps
530+ .0
531+ .get(&Mode::Insert)
532+ .unwrap()
533+ .0
534+ .get(&KeyEvent {
535+ code: KeyCode::Char('a'),
536+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
537+ })
538+ .unwrap(),
539+ Command::delete_selection
540+ );
541+ assert_eq!(
542+ *config
543+ .keymaps
544+ .0
545+ .get(&Mode::Normal)
546+ .unwrap()
547+ .0
548+ .get(&KeyEvent {
549+ code: KeyCode::F(12),
550+ modifiers: KeyModifiers::ALT
551+ })
552+ .unwrap(),
553+ Command::move_next_word_end
554+ );
555+ }
556+
557+ #[test]
558+ fn parsing_unmodified_keys() {
559+ assert_eq!(
560+ str::parse::<RepresentableKeyEvent>("backspace").unwrap(),
561+ RepresentableKeyEvent(KeyEvent {
562+ code: KeyCode::Backspace,
563+ modifiers: KeyModifiers::NONE
564+ })
565+ );
566+
567+ assert_eq!(
568+ str::parse::<RepresentableKeyEvent>("left").unwrap(),
569+ RepresentableKeyEvent(KeyEvent {
570+ code: KeyCode::Left,
571+ modifiers: KeyModifiers::NONE
572+ })
573+ );
574+
575+ assert_eq!(
576+ str::parse::<RepresentableKeyEvent>(",").unwrap(),
577+ RepresentableKeyEvent(KeyEvent {
578+ code: KeyCode::Char(','),
579+ modifiers: KeyModifiers::NONE
580+ })
581+ );
582+
583+ assert_eq!(
584+ str::parse::<RepresentableKeyEvent>("w").unwrap(),
585+ RepresentableKeyEvent(KeyEvent {
586+ code: KeyCode::Char('w'),
587+ modifiers: KeyModifiers::NONE
588+ })
589+ );
590+
591+ assert_eq!(
592+ str::parse::<RepresentableKeyEvent>("F12").unwrap(),
593+ RepresentableKeyEvent(KeyEvent {
594+ code: KeyCode::F(12),
595+ modifiers: KeyModifiers::NONE
596+ })
597+ );
598+ }
599+
600+ fn parsing_modified_keys() {
601+ assert_eq!(
602+ str::parse::<RepresentableKeyEvent>("S-minus").unwrap(),
603+ RepresentableKeyEvent(KeyEvent {
604+ code: KeyCode::Char('-'),
605+ modifiers: KeyModifiers::SHIFT
606+ })
607+ );
608+
609+ assert_eq!(
610+ str::parse::<RepresentableKeyEvent>("C-A-S-F12").unwrap(),
611+ RepresentableKeyEvent(KeyEvent {
612+ code: KeyCode::F(12),
613+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
614+ })
615+ );
616+
617+ assert_eq!(
618+ str::parse::<RepresentableKeyEvent>("S-C-2").unwrap(),
619+ RepresentableKeyEvent(KeyEvent {
620+ code: KeyCode::F(2),
621+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
622+ })
623+ );
624+ }
625+
626+ #[test]
627+ fn parsing_nonsensical_keys_fails() {
628+ assert!(str::parse::<RepresentableKeyEvent>("F13").is_err());
629+ assert!(str::parse::<RepresentableKeyEvent>("F0").is_err());
630+ assert!(str::parse::<RepresentableKeyEvent>("aaa").is_err());
631+ assert!(str::parse::<RepresentableKeyEvent>("S-S-a").is_err());
632+ assert!(str::parse::<RepresentableKeyEvent>("C-A-S-C-1").is_err());
633+ assert!(str::parse::<RepresentableKeyEvent>("FU").is_err());
634+ assert!(str::parse::<RepresentableKeyEvent>("123").is_err());
635+ assert!(str::parse::<RepresentableKeyEvent>("S--").is_err());
636+ }
637+}
helix-term/src/main.rs+8−6
@@ -1,9 +1,10 @@
1−use anyhow::{Context, Error, Result};
21use helix_term::application::Application;
32use helix_term::args::Args;
43use helix_term::config::Config;
54use std::path::PathBuf;
65
6+use anyhow::{Context, Result};
7+
78fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
89 let mut base_config = fern::Dispatch::new();
910
@@ -88,11 +89,12 @@ FLAGS:
8889 std::fs::create_dir_all(&conf_dir).ok();
8990 }
9091
91− let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
92− Ok(config) => toml::from_str(&config)?,
93− Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
94− Err(err) => return Err(Error::new(err)),
95− };
92+ let config = std::fs::read_to_string(conf_dir.join("config.toml"))
93+ .ok()
94+ .map(|s| toml::from_str(&s))
95+ .transpose()?
96+ .or_else(|| Some(Config::default()))
97+ .unwrap();
9698
9799 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
98100
helix-term/src/ui/completion.rs+40−19
@@ -238,6 +238,9 @@ impl Component for Completion {
238238 .language()
239239 .and_then(|scope| scope.strip_prefix("source."))
240240 .unwrap_or("");
241+ let cursor_pos = doc.selection(view.id).cursor();
242+ let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
243+ - view.first_line) as u16;
241244
242245 let doc = match &option.documentation {
243246 Some(lsp::Documentation::String(contents))
@@ -246,42 +249,60 @@ impl Component for Completion {
246249 value: contents,
247250 })) => {
248251 // TODO: convert to wrapped text
249− Markdown::new(format!(
250− "```{}\n{}\n```\n{}",
251− language,
252− option.detail.as_deref().unwrap_or_default(),
253− contents.clone()
254− ))
252+ Markdown::new(
253+ format!(
254+ "```{}\n{}\n```\n{}",
255+ language,
256+ option.detail.as_deref().unwrap_or_default(),
257+ contents.clone()
258+ ),
259+ cx.editor.syn_loader.clone(),
260+ )
255261 }
256262 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
257263 kind: lsp::MarkupKind::Markdown,
258264 value: contents,
259265 })) => {
260266 // TODO: set language based on doc scope
261− Markdown::new(format!(
262− "```{}\n{}\n```\n{}",
263− language,
264− option.detail.as_deref().unwrap_or_default(),
265− contents.clone()
266− ))
267+ Markdown::new(
268+ format!(
269+ "```{}\n{}\n```\n{}",
270+ language,
271+ option.detail.as_deref().unwrap_or_default(),
272+ contents.clone()
273+ ),
274+ cx.editor.syn_loader.clone(),
275+ )
267276 }
268277 None if option.detail.is_some() => {
269278 // TODO: copied from above
270279
271280 // TODO: set language based on doc scope
272− Markdown::new(format!(
273− "```{}\n{}\n```",
274− language,
275− option.detail.as_deref().unwrap_or_default(),
276− ))
281+ Markdown::new(
282+ format!(
283+ "```{}\n{}\n```",
284+ language,
285+ option.detail.as_deref().unwrap_or_default(),
286+ ),
287+ cx.editor.syn_loader.clone(),
288+ )
277289 }
278290 None => return,
279291 };
280292
281293 let half = area.height / 2;
282294 let height = 15.min(half);
283− // -2 to subtract command line + statusline. a bit of a hack, because of splits.
284− let area = Rect::new(0, area.height - height - 2, area.width, height);
295+ // we want to make sure the cursor is visible (not hidden behind the documentation)
296+ let y = if cursor_pos + view.area.y
297+ >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
298+ {
299+ 0
300+ } else {
301+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
302+ area.height.saturating_sub(height).saturating_sub(2)
303+ };
304+
305+ let area = Rect::new(0, y, area.width, height);
285306
286307 // clear area
287308 let background = cx.editor.theme.get("ui.popup");
helix-term/src/ui/editor.rs+5−7
@@ -11,13 +11,12 @@ use helix_core::{
1111 syntax::{self, HighlightEvent},
1212 LineEnding, Position, Range,
1313};
14−use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
1514use helix_view::{document::Mode, Document, Editor, Theme, View};
1615use std::borrow::Cow;
1716
1817use crossterm::{
1918 cursor,
20− event::{read, Event, EventStream},
19+ event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
2120};
2221use tui::{
2322 backend::CrosstermBackend,
@@ -130,7 +129,7 @@ impl EditorView {
130129 })],
131130 };
132131 let mut spans = Vec::new();
133− let mut visual_x = 0;
132+ let mut visual_x = 0u16;
134133 let mut line = 0u16;
135134 let tab_width = doc.tab_width();
136135
@@ -186,7 +185,7 @@ impl EditorView {
186185 break 'outer;
187186 }
188187 } else if grapheme == "\t" {
189− visual_x += (tab_width as u16);
188+ visual_x = visual_x.saturating_add(tab_width as u16);
190189 } else {
191190 let out_of_bounds = visual_x < view.first_col as u16
192191 || visual_x >= viewport.width + view.first_col as u16;
@@ -198,7 +197,7 @@ impl EditorView {
198197
199198 if out_of_bounds {
200199 // if we're offscreen just keep going until we hit a new line
201− visual_x += width;
200+ visual_x = visual_x.saturating_add(width);
202201 continue;
203202 }
204203
@@ -608,8 +607,7 @@ impl Component for EditorView {
608607 cx.editor.resize(Rect::new(0, 0, width, height - 1));
609608 EventResult::Consumed(None)
610609 }
611− Event::Key(key) => {
612− let mut key = KeyEvent::from(key);
610+ Event::Key(mut key) => {
613611 canonicalize_key(&mut key);
614612 // clear status
615613 cx.editor.status_msg = None;
helix-term/src/ui/markdown.rs+18−14
@@ -7,25 +7,34 @@ use tui::{
77 text::Text,
88};
99
10−use std::borrow::Cow;
10+use std::{borrow::Cow, sync::Arc};
1111
12−use helix_core::Position;
12+use helix_core::{syntax, Position};
1313use helix_view::{Editor, Theme};
1414
1515pub struct Markdown {
1616 contents: String,
17+
18+ config_loader: Arc<syntax::Loader>,
1719}
1820
1921// TODO: pre-render and self reference via Pin
2022// better yet, just use Tendril + subtendril for references
2123
2224impl Markdown {
23− pub fn new(contents: String) -> Self {
24− Self { contents }
25+ pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
26+ Self {
27+ contents,
28+ config_loader,
29+ }
2530 }
2631}
2732
28−fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
33+fn parse<'a>(
34+ contents: &'a str,
35+ theme: Option<&Theme>,
36+ loader: &syntax::Loader,
37+) -> tui::text::Text<'a> {
2938 use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
3039 use tui::text::{Span, Spans, Text};
3140
@@ -79,9 +88,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
7988 use helix_core::Rope;
8089
8190 let rope = Rope::from(text.as_ref());
82− let syntax = syntax::LOADER
83− .get()
84− .unwrap()
91+ let syntax = loader
8592 .language_config_for_scope(&format!("source.{}", language))
8693 .and_then(|config| config.highlight_config(theme.scopes()))
8794 .map(|config| Syntax::new(&rope, config));
@@ -101,9 +108,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
101108 }
102109 HighlightEvent::Source { start, end } => {
103110 let style = match highlights.first() {
104− Some(span) => {
105− theme.get(theme.scopes()[span.0].as_str())
106− }
111+ Some(span) => theme.get(&theme.scopes()[span.0]),
107112 None => text_style,
108113 };
109114
@@ -159,7 +164,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
159164 }
160165 }
161166 Event::Code(text) | Event::Html(text) => {
162− log::warn!("code {:?}", text);
163167 let mut span = to_span(text);
164168 span.style = code_style;
165169 spans.push(span);
@@ -198,7 +202,7 @@ impl Component for Markdown {
198202 fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
199203 use tui::widgets::{Paragraph, Widget, Wrap};
200204
201− let text = parse(&self.contents, Some(&cx.editor.theme));
205+ let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
202206
203207 let par = Paragraph::new(text)
204208 .wrap(Wrap { trim: false })
@@ -209,7 +213,7 @@ impl Component for Markdown {
209213 }
210214
211215 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
212− let contents = parse(&self.contents, None);
216+ let contents = parse(&self.contents, None, &self.config_loader);
213217 let padding = 2;
214218 let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
215219 let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
helix-term/src/ui/mod.rs+34−5
@@ -115,10 +115,43 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
115115
116116pub mod completers {
117117 use crate::ui::prompt::Completion;
118− use std::borrow::Cow;
118+ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
119+ use fuzzy_matcher::FuzzyMatcher;
120+ use helix_view::theme;
121+ use std::cmp::Reverse;
122+ use std::{borrow::Cow, sync::Arc};
119123
120124 pub type Completer = fn(&str) -> Vec<Completion>;
121125
126+ pub fn theme(input: &str) -> Vec<Completion> {
127+ let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
128+ names.extend(theme::Loader::read_names(
129+ &helix_core::config_dir().join("themes"),
130+ ));
131+ names.push("default".into());
132+
133+ let mut names: Vec<_> = names
134+ .into_iter()
135+ .map(|name| ((0..), Cow::from(name)))
136+ .collect();
137+
138+ let matcher = Matcher::default();
139+
140+ let mut matches: Vec<_> = names
141+ .into_iter()
142+ .filter_map(|(range, name)| {
143+ matcher
144+ .fuzzy_match(&name, &input)
145+ .map(|score| (name, score))
146+ })
147+ .collect();
148+
149+ matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
150+ names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
151+
152+ names
153+ }
154+
122155 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
123156 pub fn filename(input: &str) -> Vec<Completion> {
124157 // Rust's filename handling is really annoying.
@@ -178,10 +211,6 @@ pub mod completers {
178211
179212 // if empty, return a list of dirs and files in current dir
180213 if let Some(file_name) = file_name {
181− use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
182− use fuzzy_matcher::FuzzyMatcher;
183− use std::cmp::Reverse;
184−
185214 let matcher = Matcher::default();
186215
187216 // inefficient, but we need to calculate the scores, filter out None, then sort.
helix-term/src/ui/prompt.rs+187−49
@@ -6,6 +6,11 @@ use helix_view::{Editor, Theme};
66use std::{borrow::Cow, ops::RangeFrom};
77use tui::terminal::CursorKind;
88
9+use helix_core::{
10+ unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
11+ unicode::width::UnicodeWidthStr,
12+};
13+
914pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
1015
1116pub struct Prompt {
@@ -34,6 +39,17 @@ pub enum CompletionDirection {
3439 Backward,
3540}
3641
42+#[derive(Debug, Clone, Copy)]
43+pub enum Movement {
44+ BackwardChar(usize),
45+ BackwardWord(usize),
46+ ForwardChar(usize),
47+ ForwardWord(usize),
48+ StartOfLine,
49+ EndOfLine,
50+ None,
51+}
52+
3753impl Prompt {
3854 pub fn new(
3955 prompt: String,
@@ -52,30 +68,120 @@ impl Prompt {
5268 }
5369 }
5470
71+ /// Compute the cursor position after applying movement
72+ /// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
73+ fn eval_movement(&self, movement: Movement) -> usize {
74+ match movement {
75+ Movement::BackwardChar(rep) => {
76+ let mut position = self.cursor;
77+ for _ in 0..rep {
78+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
79+ if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
80+ position = pos;
81+ } else {
82+ break;
83+ }
84+ }
85+ position
86+ }
87+ Movement::BackwardWord(rep) => {
88+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
89+ if char_indices.is_empty() {
90+ return self.cursor;
91+ }
92+ let mut char_position = char_indices
93+ .iter()
94+ .position(|(idx, _)| *idx == self.cursor)
95+ .unwrap_or(char_indices.len() - 1);
96+
97+ for _ in 0..rep {
98+ if char_position == 0 {
99+ break;
100+ }
101+
102+ let mut found = None;
103+ for prev in (0..char_position - 1).rev() {
104+ if char_indices[prev].1.is_whitespace() {
105+ found = Some(prev + 1);
106+ break;
107+ }
108+ }
109+
110+ char_position = found.unwrap_or(0);
111+ }
112+ char_indices[char_position].0
113+ }
114+ Movement::ForwardWord(rep) => {
115+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
116+ if char_indices.is_empty() {
117+ return self.cursor;
118+ }
119+ let mut char_position = char_indices
120+ .iter()
121+ .position(|(idx, _)| *idx == self.cursor)
122+ .unwrap_or_else(|| char_indices.len());
123+
124+ for _ in 0..rep {
125+ // Skip any non-whitespace characters
126+ while char_position < char_indices.len()
127+ && !char_indices[char_position].1.is_whitespace()
128+ {
129+ char_position += 1;
130+ }
131+
132+ // Skip any whitespace characters
133+ while char_position < char_indices.len()
134+ && char_indices[char_position].1.is_whitespace()
135+ {
136+ char_position += 1;
137+ }
138+
139+ // We are now on the start of the next word
140+ }
141+ char_indices
142+ .get(char_position)
143+ .map(|(i, _)| *i)
144+ .unwrap_or_else(|| self.line.len())
145+ }
146+ Movement::ForwardChar(rep) => {
147+ let mut position = self.cursor;
148+ for _ in 0..rep {
149+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
150+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
151+ position = pos;
152+ } else {
153+ break;
154+ }
155+ }
156+ position
157+ }
158+ Movement::StartOfLine => 0,
159+ Movement::EndOfLine => {
160+ let mut cursor =
161+ GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
162+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
163+ pos
164+ } else {
165+ self.cursor
166+ }
167+ }
168+ Movement::None => self.cursor,
169+ }
170+ }
171+
55172 pub fn insert_char(&mut self, c: char) {
56− let pos = if self.line.is_empty() {
57− 0
58− } else {
59− self.line
60− .char_indices()
61− .nth(self.cursor)
62− .map(|(pos, _)| pos)
63− .unwrap_or_else(|| self.line.len())
64− };
65− self.line.insert(pos, c);
66− self.cursor += 1;
173+ self.line.insert(self.cursor, c);
174+ let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
175+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
176+ self.cursor = pos;
177+ }
67178 self.completion = (self.completion_fn)(&self.line);
68179 self.exit_selection();
69180 }
70181
71− pub fn move_char_left(&mut self) {
72− self.cursor = self.cursor.saturating_sub(1)
73− }
74−
75− pub fn move_char_right(&mut self) {
76− if self.cursor < self.line.len() {
77− self.cursor += 1;
78− }
182+ pub fn move_cursor(&mut self, movement: Movement) {
183+ let pos = self.eval_movement(movement);
184+ self.cursor = pos
79185 }
80186
81187 pub fn move_start(&mut self) {
@@ -87,39 +193,29 @@ impl Prompt {
87193 }
88194
89195 pub fn delete_char_backwards(&mut self) {
90− if self.cursor > 0 {
91− let pos = self
92− .line
93− .char_indices()
94− .nth(self.cursor - 1)
95− .map(|(pos, _)| pos)
96− .expect("line is not empty");
97− self.line.remove(pos);
98− self.cursor -= 1;
99− self.completion = (self.completion_fn)(&self.line);
100− }
196+ let pos = self.eval_movement(Movement::BackwardChar(1));
197+ self.line.replace_range(pos..self.cursor, "");
198+ self.cursor = pos;
199+
101200 self.exit_selection();
201+ self.completion = (self.completion_fn)(&self.line);
102202 }
103203
104204 pub fn delete_word_backwards(&mut self) {
105− use helix_core::get_general_category;
106− let mut chars = self.line.char_indices().rev();
107− // TODO add skipping whitespace logic here
108− let (mut i, cat) = match chars.next() {
109− Some((i, c)) => (i, get_general_category(c)),
110− None => return,
111− };
112− self.cursor -= 1;
113− for (nn, nc) in chars {
114− if get_general_category(nc) != cat {
115− break;
116− }
117− i = nn;
118− self.cursor -= 1;
119− }
120− self.line.drain(i..);
205+ let pos = self.eval_movement(Movement::BackwardWord(1));
206+ self.line.replace_range(pos..self.cursor, "");
207+ self.cursor = pos;
208+
209+ self.exit_selection();
121210 self.completion = (self.completion_fn)(&self.line);
211+ }
212+
213+ pub fn kill_to_end_of_line(&mut self) {
214+ let pos = self.eval_movement(Movement::EndOfLine);
215+ self.line.replace_range(self.cursor..pos, "");
216+
122217 self.exit_selection();
218+ self.completion = (self.completion_fn)(&self.line);
123219 }
124220
125221 pub fn clear(&mut self) {
@@ -293,31 +389,71 @@ impl Component for Prompt {
293389 (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
294390 }
295391 KeyEvent {
392+ code: KeyCode::Char('c'),
393+ modifiers: KeyModifiers::CONTROL,
394+ }
395+ | KeyEvent {
296396 code: KeyCode::Esc, ..
297397 } => {
298398 (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
299399 return close_fn;
300400 }
301401 KeyEvent {
402+ code: KeyCode::Char('f'),
403+ modifiers: KeyModifiers::CONTROL,
404+ }
405+ | KeyEvent {
302406 code: KeyCode::Right,
303407 ..
304− } => self.move_char_right(),
408+ } => self.move_cursor(Movement::ForwardChar(1)),
305409 KeyEvent {
410+ code: KeyCode::Char('b'),
411+ modifiers: KeyModifiers::CONTROL,
412+ }
413+ | KeyEvent {
306414 code: KeyCode::Left,
307415 ..
308− } => self.move_char_left(),
416+ } => self.move_cursor(Movement::BackwardChar(1)),
309417 KeyEvent {
418+ code: KeyCode::End,
419+ modifiers: KeyModifiers::NONE,
420+ }
421+ | KeyEvent {
310422 code: KeyCode::Char('e'),
311423 modifiers: KeyModifiers::CONTROL,
312424 } => self.move_end(),
313425 KeyEvent {
426+ code: KeyCode::Home,
427+ modifiers: KeyModifiers::NONE,
428+ }
429+ | KeyEvent {
314430 code: KeyCode::Char('a'),
315431 modifiers: KeyModifiers::CONTROL,
316432 } => self.move_start(),
433+ KeyEvent {
434+ code: KeyCode::Left,
435+ modifiers: KeyModifiers::ALT,
436+ }
437+ | KeyEvent {
438+ code: KeyCode::Char('b'),
439+ modifiers: KeyModifiers::ALT,
440+ } => self.move_cursor(Movement::BackwardWord(1)),
441+ KeyEvent {
442+ code: KeyCode::Right,
443+ modifiers: KeyModifiers::ALT,
444+ }
445+ | KeyEvent {
446+ code: KeyCode::Char('f'),
447+ modifiers: KeyModifiers::ALT,
448+ } => self.move_cursor(Movement::ForwardWord(1)),
317449 KeyEvent {
318450 code: KeyCode::Char('w'),
319451 modifiers: KeyModifiers::CONTROL,
320452 } => self.delete_word_backwards(),
453+ KeyEvent {
454+ code: KeyCode::Char('k'),
455+ modifiers: KeyModifiers::CONTROL,
456+ } => self.kill_to_end_of_line(),
321457 KeyEvent {
322458 code: KeyCode::Backspace,
323459 modifiers: KeyModifiers::NONE,
@@ -363,7 +499,9 @@ impl Component for Prompt {
363499 (
364500 Some(Position::new(
365501 area.y as usize + line,
366− area.x as usize + self.prompt.len() + self.cursor,
502+ area.x as usize
503+ + self.prompt.len()
504+ + UnicodeWidthStr::width(&self.line[..self.cursor]),
367505 )),
368506 CursorKind::Block,
369507 )
helix-tui/src/buffer.rs+2−19
@@ -203,16 +203,6 @@ impl Buffer {
203203 /// # Panics
204204 ///
205205 /// Panics when given an coordinate that is outside of this Buffer's area.
206− ///
207− /// ```should_panic
208− /// # use helix_tui::buffer::Buffer;
209− /// # use helix_tui::layout::Rect;
210− /// let rect = Rect::new(200, 100, 10, 10);
211− /// let buffer = Buffer::empty(rect);
212− /// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
213− /// // starts at (200, 100).
214− /// buffer.index_of(0, 0); // Panics
215− /// ```
216206 pub fn index_of(&self, x: u16, y: u16) -> usize {
217207 debug_assert!(
218208 x >= self.area.left()
@@ -245,15 +235,6 @@ impl Buffer {
245235 /// # Panics
246236 ///
247237 /// Panics when given an index that is outside the Buffer's content.
248− ///
249− /// ```should_panic
250− /// # use helix_tui::buffer::Buffer;
251− /// # use helix_tui::layout::Rect;
252− /// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
253− /// let buffer = Buffer::empty(rect);
254− /// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
255− /// buffer.pos_of(100); // Panics
256− /// ```
257238 pub fn pos_of(&self, i: usize) -> (u16, u16) {
258239 debug_assert!(
259240 i < self.content.len(),
@@ -510,6 +491,7 @@ mod tests {
510491
511492 #[test]
512493 #[should_panic(expected = "outside the buffer")]
494+ #[cfg(debug_assertions)]
513495 fn pos_of_panics_on_out_of_bounds() {
514496 let rect = Rect::new(0, 0, 10, 10);
515497 let buf = Buffer::empty(rect);
@@ -520,6 +502,7 @@ mod tests {
520502
521503 #[test]
522504 #[should_panic(expected = "outside the buffer")]
505+ #[cfg(debug_assertions)]
523506 fn index_of_panics_on_out_of_bounds() {
524507 let rect = Rect::new(0, 0, 10, 10);
525508 let buf = Buffer::empty(rect);
helix-tui/src/lib.rs+1−1
@@ -44,7 +44,7 @@
4444//! implement your own.
4545//!
4646//! Each widget follows a builder pattern API providing a default configuration along with methods
47−//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
47+//! to customize them. The widget is then rendered using the `Frame::render_widget` which take
4848//! your widget instance an area to draw to.
4949//!
5050//! The following example renders a block of the size of the terminal:
helix-tui/src/widgets/mod.rs+1−1
@@ -1,4 +1,4 @@
1−//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
1+//! `widgets` is a collection of types that implement [`Widget`].
22//!
33//! All widgets are implemented using the builder pattern and are consumable objects. They are not
44//! meant to be stored but used as *commands* to draw common figures in the UI.
helix-view/Cargo.toml+3
@@ -34,3 +34,6 @@ slotmap = "1"
3434serde = { version = "1.0", features = ["derive"] }
3535toml = "0.5"
3636log = "~0.4"
37+
38+which = "4.1"
39+
helix-view/src/clipboard.rsnew file+193
@@ -0,0 +1,193 @@
1+// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
2+
3+use anyhow::Result;
4+use std::borrow::Cow;
5+
6+pub trait ClipboardProvider: std::fmt::Debug {
7+ fn name(&self) -> Cow<str>;
8+ fn get_contents(&self) -> Result<String>;
9+ fn set_contents(&self, contents: String) -> Result<()>;
10+}
11+
12+macro_rules! command_provider {
13+ (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
14+ Box::new(provider::CommandProvider {
15+ get_cmd: provider::CommandConfig {
16+ prg: $get_prg,
17+ args: &[ $( $get_arg ),* ],
18+ },
19+ set_cmd: provider::CommandConfig {
20+ prg: $set_prg,
21+ args: &[ $( $set_arg ),* ],
22+ },
23+ })
24+ }};
25+}
26+
27+pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
28+ // TODO: support for user-defined provider, probably when we have plugin support by setting a
29+ // variable?
30+
31+ if exists("pbcopy") && exists("pbpaste") {
32+ command_provider! {
33+ paste => "pbpaste";
34+ copy => "pbcopy";
35+ }
36+ } else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
37+ command_provider! {
38+ paste => "wl-paste", "--no-newline";
39+ copy => "wl-copy", "--foreground", "--type", "text/plain";
40+ }
41+ } else if env_var_is_set("DISPLAY") && exists("xclip") {
42+ command_provider! {
43+ paste => "xclip", "-o", "-selection", "clipboard";
44+ copy => "xclip", "-i", "-selection", "clipboard";
45+ }
46+ } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
47+ {
48+ // FIXME: check performance of is_exit_success
49+ command_provider! {
50+ paste => "xsel", "-o", "-b";
51+ copy => "xsel", "--nodetach", "-i", "-b";
52+ }
53+ } else if exists("lemonade") {
54+ command_provider! {
55+ paste => "lemonade", "paste";
56+ copy => "lemonade", "copy";
57+ }
58+ } else if exists("doitclient") {
59+ command_provider! {
60+ paste => "doitclient", "wclip", "-r";
61+ copy => "doitclient", "wclip";
62+ }
63+ } else if exists("win32yank.exe") {
64+ // FIXME: does it work within WSL?
65+ command_provider! {
66+ paste => "win32yank.exe", "-o", "--lf";
67+ copy => "win32yank.exe", "-i", "--crlf";
68+ }
69+ } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
70+ command_provider! {
71+ paste => "termux-clipboard-get";
72+ copy => "termux-clipboard-set";
73+ }
74+ } else if env_var_is_set("TMUX") && exists("tmux") {
75+ command_provider! {
76+ paste => "tmux", "save-buffer", "-";
77+ copy => "tmux", "load-buffer", "-";
78+ }
79+ } else {
80+ Box::new(provider::NopProvider)
81+ }
82+}
83+
84+fn exists(executable_name: &str) -> bool {
85+ which::which(executable_name).is_ok()
86+}
87+
88+fn env_var_is_set(env_var_name: &str) -> bool {
89+ std::env::var_os(env_var_name).is_some()
90+}
91+
92+fn is_exit_success(program: &str, args: &[&str]) -> bool {
93+ std::process::Command::new(program)
94+ .args(args)
95+ .output()
96+ .ok()
97+ .and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
98+ .is_some()
99+}
100+
101+mod provider {
102+ use super::ClipboardProvider;
103+ use anyhow::{bail, Context as _, Result};
104+ use std::borrow::Cow;
105+
106+ #[derive(Debug)]
107+ pub struct NopProvider;
108+
109+ impl ClipboardProvider for NopProvider {
110+ fn name(&self) -> Cow<str> {
111+ Cow::Borrowed("none")
112+ }
113+
114+ fn get_contents(&self) -> Result<String> {
115+ Ok(String::new())
116+ }
117+
118+ fn set_contents(&self, _: String) -> Result<()> {
119+ Ok(())
120+ }
121+ }
122+
123+ #[derive(Debug)]
124+ pub struct CommandConfig {
125+ pub prg: &'static str,
126+ pub args: &'static [&'static str],
127+ }
128+
129+ impl CommandConfig {
130+ fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
131+ use std::io::Write;
132+ use std::process::{Command, Stdio};
133+
134+ let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
135+ let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
136+
137+ let mut child = Command::new(self.prg)
138+ .args(self.args)
139+ .stdin(stdin)
140+ .stdout(stdout)
141+ .stderr(Stdio::null())
142+ .spawn()?;
143+
144+ if let Some(input) = input {
145+ let mut stdin = child.stdin.take().context("stdin is missing")?;
146+ stdin
147+ .write_all(input.as_bytes())
148+ .context("couldn't write in stdin")?;
149+ }
150+
151+ // TODO: add timer?
152+ let output = child.wait_with_output()?;
153+
154+ if !output.status.success() {
155+ bail!("clipboard provider {} failed", self.prg);
156+ }
157+
158+ if pipe_output {
159+ Ok(Some(String::from_utf8(output.stdout)?))
160+ } else {
161+ Ok(None)
162+ }
163+ }
164+ }
165+
166+ #[derive(Debug)]
167+ pub struct CommandProvider {
168+ pub get_cmd: CommandConfig,
169+ pub set_cmd: CommandConfig,
170+ }
171+
172+ impl ClipboardProvider for CommandProvider {
173+ fn name(&self) -> Cow<str> {
174+ if self.get_cmd.prg != self.set_cmd.prg {
175+ Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
176+ } else {
177+ Cow::Borrowed(self.get_cmd.prg)
178+ }
179+ }
180+
181+ fn get_contents(&self) -> Result<String> {
182+ let output = self
183+ .get_cmd
184+ .execute(None, true)?
185+ .context("output is missing")?;
186+ Ok(output)
187+ }
188+
189+ fn set_contents(&self, value: String) -> Result<()> {
190+ self.set_cmd.execute(Some(&value), false).map(|_| ())
191+ }
192+ }
193+}
helix-view/src/document.rs+51−55
@@ -1,7 +1,5 @@
11use anyhow::{anyhow, Context, Error};
2−use serde::de::{self, Deserialize, Deserializer};
32use std::cell::Cell;
4−use std::collections::HashMap;
53use std::fmt::Display;
64use std::future::Future;
75use std::path::{Component, Path, PathBuf};
@@ -12,12 +10,14 @@ use helix_core::{
1210 auto_detect_line_ending,
1311 chars::{char_is_line_ending, char_is_whitespace},
1412 history::History,
15− syntax::{LanguageConfiguration, LOADER},
13+ syntax::{self, LanguageConfiguration},
1614 ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
1715 DEFAULT_LINE_ENDING,
1816};
1917
20−use crate::{DocumentId, ViewId};
18+use crate::{DocumentId, Theme, ViewId};
19+
20+use std::collections::HashMap;
2121
2222#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
2323pub enum Mode {
@@ -26,40 +26,6 @@ pub enum Mode {
2626 Insert,
2727}
2828
29−impl Display for Mode {
30− fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31− match self {
32− Mode::Normal => f.write_str("normal"),
33− Mode::Select => f.write_str("select"),
34− Mode::Insert => f.write_str("insert"),
35− }
36− }
37−}
38−
39−impl FromStr for Mode {
40− type Err = Error;
41−
42− fn from_str(s: &str) -> Result<Self, Self::Err> {
43− match s {
44− "normal" => Ok(Mode::Normal),
45− "select" => Ok(Mode::Select),
46− "insert" => Ok(Mode::Insert),
47− _ => Err(anyhow!("Invalid mode '{}'", s)),
48− }
49− }
50−}
51−
52−// toml deserializer doesn't seem to recognize string as enum
53−impl<'de> Deserialize<'de> for Mode {
54− fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55− where
56− D: Deserializer<'de>,
57− {
58− let s = String::deserialize(deserializer)?;
59− s.parse().map_err(de::Error::custom)
60− }
61−}
62−
6329#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
6430pub enum IndentStyle {
6531 Tabs,
@@ -127,6 +93,29 @@ impl fmt::Debug for Document {
12793 }
12894}
12995
96+impl Display for Mode {
97+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98+ match self {
99+ Mode::Normal => f.write_str("normal"),
100+ Mode::Select => f.write_str("select"),
101+ Mode::Insert => f.write_str("insert"),
102+ }
103+ }
104+}
105+
106+impl FromStr for Mode {
107+ type Err = Error;
108+
109+ fn from_str(s: &str) -> Result<Self, Self::Err> {
110+ match s {
111+ "normal" => Ok(Mode::Normal),
112+ "select" => Ok(Mode::Select),
113+ "insert" => Ok(Mode::Insert),
114+ _ => Err(anyhow!("Invalid mode '{}'", s)),
115+ }
116+ }
117+}
118+
130119/// Like std::mem::replace() except it allows the replacement value to be mapped from the
131120/// original value.
132121fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -181,7 +170,7 @@ pub fn fold_home_dir(path: &Path) -> PathBuf {
181170/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
182171/// fail, or on Windows returns annoying device paths. This is a problem Cargo
183172/// needs to improve on.
184−/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
173+/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
185174pub fn normalize_path(path: &Path) -> PathBuf {
186175 let path = expand_tilde(path);
187176 let mut components = path.components().peekable();
@@ -253,7 +242,11 @@ impl Document {
253242 }
254243
255244 // TODO: async fn?
256− pub fn load(path: PathBuf) -> Result<Self, Error> {
245+ pub fn load(
246+ path: PathBuf,
247+ theme: Option<&Theme>,
248+ config_loader: Option<&syntax::Loader>,
249+ ) -> Result<Self, Error> {
257250 use std::{fs::File, io::BufReader};
258251
259252 let mut doc = if !path.exists() {
@@ -277,6 +270,10 @@ impl Document {
277270 doc.detect_indent_style();
278271 doc.set_line_ending(line_ending);
279272
273+ if let Some(loader) = config_loader {
274+ doc.detect_language(theme, loader);
275+ }
276+
280277 Ok(doc)
281278 }
282279
@@ -351,12 +348,10 @@ impl Document {
351348 }
352349 }
353350
354− fn detect_language(&mut self) {
355− if let Some(path) = self.path() {
356− let loader = LOADER.get().unwrap();
357− let language_config = loader.language_config_for_file_name(path);
358− let scopes = loader.scopes();
359− self.set_language(language_config, scopes);
351+ pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
352+ if let Some(path) = &self.path {
353+ let language_config = config_loader.language_config_for_file_name(path);
354+ self.set_language(theme, language_config);
360355 }
361356 }
362357
@@ -493,18 +488,16 @@ impl Document {
493488 // and error out when document is saved
494489 self.path = Some(path);
495490
496− // try detecting the language based on filepath
497− self.detect_language();
498−
499491 Ok(())
500492 }
501493
502494 pub fn set_language(
503495 &mut self,
496+ theme: Option<&Theme>,
504497 language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
505− scopes: &[String],
506498 ) {
507499 if let Some(language_config) = language_config {
500+ let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
508501 if let Some(highlight_config) = language_config.highlight_config(scopes) {
509502 let syntax = Syntax::new(&self.text, highlight_config);
510503 self.syntax = Some(syntax);
@@ -518,12 +511,15 @@ impl Document {
518511 };
519512 }
520513
521− pub fn set_language2(&mut self, scope: &str) {
522− let loader = LOADER.get().unwrap();
523− let language_config = loader.language_config_for_scope(scope);
524− let scopes = loader.scopes();
514+ pub fn set_language2(
515+ &mut self,
516+ scope: &str,
517+ theme: Option<&Theme>,
518+ config_loader: Arc<syntax::Loader>,
519+ ) {
520+ let language_config = config_loader.language_config_for_scope(scope);
525521
526− self.set_language(language_config, scopes);
522+ self.set_language(theme, language_config);
527523 }
528524
529525 pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
helix-view/src/editor.rs+52−26
@@ -1,10 +1,15 @@
1−use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
1+use crate::clipboard::{get_clipboard_provider, ClipboardProvider};
2+use crate::{
3+ theme::{self, Theme},
4+ tree::Tree,
5+ Document, DocumentId, RegisterSelection, View, ViewId,
6+};
7+use helix_core::syntax;
28use tui::layout::Rect;
39use tui::terminal::CursorKind;
410
511use futures_util::future;
6−use std::path::PathBuf;
7−use std::time::Duration;
12+use std::{path::PathBuf, sync::Arc, time::Duration};
813
914use slotmap::SlotMap;
1015
@@ -23,6 +28,10 @@ pub struct Editor {
2328 pub registers: Registers,
2429 pub theme: Theme,
2530 pub language_servers: helix_lsp::Registry,
31+ pub clipboard_provider: Box<dyn ClipboardProvider>,
32+
33+ pub syn_loader: Arc<syntax::Loader>,
34+ pub theme_loader: Arc<theme::Loader>,
2635
2736 pub status_msg: Option<(String, Severity)>,
2837}
@@ -35,27 +44,11 @@ pub enum Action {
3544}
3645
3746impl Editor {
38− pub fn new(mut area: tui::layout::Rect) -> Self {
39− use helix_core::config_dir;
40− let config = std::fs::read(config_dir().join("theme.toml"));
41− // load $HOME/.config/helix/theme.toml, fallback to default config
42− let toml = config
43− .as_deref()
44− .unwrap_or(include_bytes!("../../theme.toml"));
45− let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
46−
47− // initialize language registry
48− use helix_core::syntax::{Loader, LOADER};
49−
50− // load $HOME/.config/helix/languages.toml, fallback to default config
51− let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
52− let toml = config
53− .as_deref()
54− .unwrap_or(include_bytes!("../../languages.toml"));
55−
56− let config = toml::from_slice(toml).expect("Could not parse languages.toml");
57− LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
58−
47+ pub fn new(
48+ mut area: tui::layout::Rect,
49+ themes: Arc<theme::Loader>,
50+ config_loader: Arc<syntax::Loader>,
51+ ) -> Self {
5952 let language_servers = helix_lsp::Registry::new();
6053
6154 // HAXX: offset the render area height by 1 to account for prompt/commandline
@@ -66,9 +59,12 @@ impl Editor {
6659 documents: SlotMap::with_key(),
6760 count: None,
6861 selected_register: RegisterSelection::default(),
69− theme,
62+ theme: themes.default(),
7063 language_servers,
64+ syn_loader: config_loader,
65+ theme_loader: themes,
7166 registers: Registers::default(),
67+ clipboard_provider: get_clipboard_provider(),
7268 status_msg: None,
7369 }
7470 }
@@ -85,6 +81,32 @@ impl Editor {
8581 self.status_msg = Some((error, Severity::Error));
8682 }
8783
84+ pub fn set_theme(&mut self, theme: Theme) {
85+ let scopes = theme.scopes();
86+ for config in self
87+ .syn_loader
88+ .language_configs_iter()
89+ .filter(|cfg| cfg.is_highlight_initialized())
90+ {
91+ config.reconfigure(scopes);
92+ }
93+
94+ self.theme = theme;
95+ self._refresh();
96+ }
97+
98+ pub fn set_theme_from_name(&mut self, theme: &str) {
99+ let theme = match self.theme_loader.load(theme.as_ref()) {
100+ Ok(theme) => theme,
101+ Err(e) => {
102+ log::warn!("failed setting theme `{}` - {}", theme, e);
103+ return;
104+ }
105+ };
106+
107+ self.set_theme(theme);
108+ }
109+
88110 fn _refresh(&mut self) {
89111 for (view, _) in self.tree.views_mut() {
90112 let doc = &self.documents[view.doc];
@@ -168,7 +190,7 @@ impl Editor {
168190 let id = if let Some(id) = id {
169191 id
170192 } else {
171− let mut doc = Document::load(path)?;
193+ let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?;
172194
173195 // try to find a language server based on the language name
174196 let language_server = doc
@@ -254,6 +276,10 @@ impl Editor {
254276 self.documents.iter().map(|(_id, doc)| doc)
255277 }
256278
279+ pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
280+ self.documents.iter_mut().map(|(_id, doc)| doc)
281+ }
282+
257283 // pub fn current_document(&self) -> Document {
258284 // let id = self.view().doc;
259285 // let doc = &mut editor.documents[id];
helix-view/src/input.rsdeleted−226
@@ -1,226 +0,0 @@
1−//! Input event handling, currently backed by crossterm.
2−use anyhow::{anyhow, Error};
3−use crossterm::event;
4−use serde::de::{self, Deserialize, Deserializer};
5−use std::fmt;
6−
7−pub use crossterm::event::{KeyCode, KeyModifiers};
8−
9−/// Represents a key event.
10−// We use a newtype here because we want to customize Deserialize and Display.
11−#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
12−pub struct KeyEvent {
13− pub code: KeyCode,
14− pub modifiers: KeyModifiers,
15−}
16−
17−impl fmt::Display for KeyEvent {
18− fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
19− f.write_fmt(format_args!(
20− "{}{}{}",
21− if self.modifiers.contains(KeyModifiers::SHIFT) {
22− "S-"
23− } else {
24− ""
25− },
26− if self.modifiers.contains(KeyModifiers::ALT) {
27− "A-"
28− } else {
29− ""
30− },
31− if self.modifiers.contains(KeyModifiers::CONTROL) {
32− "C-"
33− } else {
34− ""
35− },
36− ))?;
37− match self.code {
38− KeyCode::Backspace => f.write_str("backspace")?,
39− KeyCode::Enter => f.write_str("ret")?,
40− KeyCode::Left => f.write_str("left")?,
41− KeyCode::Right => f.write_str("right")?,
42− KeyCode::Up => f.write_str("up")?,
43− KeyCode::Down => f.write_str("down")?,
44− KeyCode::Home => f.write_str("home")?,
45− KeyCode::End => f.write_str("end")?,
46− KeyCode::PageUp => f.write_str("pageup")?,
47− KeyCode::PageDown => f.write_str("pagedown")?,
48− KeyCode::Tab => f.write_str("tab")?,
49− KeyCode::BackTab => f.write_str("backtab")?,
50− KeyCode::Delete => f.write_str("del")?,
51− KeyCode::Insert => f.write_str("ins")?,
52− KeyCode::Null => f.write_str("null")?,
53− KeyCode::Esc => f.write_str("esc")?,
54− KeyCode::Char('<') => f.write_str("lt")?,
55− KeyCode::Char('>') => f.write_str("gt")?,
56− KeyCode::Char('+') => f.write_str("plus")?,
57− KeyCode::Char('-') => f.write_str("minus")?,
58− KeyCode::Char(';') => f.write_str("semicolon")?,
59− KeyCode::Char('%') => f.write_str("percent")?,
60− KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
61− KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
62− };
63− Ok(())
64− }
65−}
66−
67−impl std::str::FromStr for KeyEvent {
68− type Err = Error;
69−
70− fn from_str(s: &str) -> Result<Self, Self::Err> {
71− let mut tokens: Vec<_> = s.split('-').collect();
72− let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
73− "backspace" => KeyCode::Backspace,
74− "space" => KeyCode::Char(' '),
75− "ret" => KeyCode::Enter,
76− "lt" => KeyCode::Char('<'),
77− "gt" => KeyCode::Char('>'),
78− "plus" => KeyCode::Char('+'),
79− "minus" => KeyCode::Char('-'),
80− "semicolon" => KeyCode::Char(';'),
81− "percent" => KeyCode::Char('%'),
82− "left" => KeyCode::Left,
83− "right" => KeyCode::Right,
84− "up" => KeyCode::Down,
85− "home" => KeyCode::Home,
86− "end" => KeyCode::End,
87− "pageup" => KeyCode::PageUp,
88− "pagedown" => KeyCode::PageDown,
89− "tab" => KeyCode::Tab,
90− "backtab" => KeyCode::BackTab,
91− "del" => KeyCode::Delete,
92− "ins" => KeyCode::Insert,
93− "null" => KeyCode::Null,
94− "esc" => KeyCode::Esc,
95− single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
96− function if function.len() > 1 && function.starts_with('F') => {
97− let function: String = function.chars().skip(1).collect();
98− let function = str::parse::<u8>(&function)?;
99− (function > 0 && function < 13)
100− .then(|| KeyCode::F(function))
101− .ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
102− }
103− invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
104− };
105−
106− let mut modifiers = KeyModifiers::empty();
107− for token in tokens {
108− let flag = match token {
109− "S" => KeyModifiers::SHIFT,
110− "A" => KeyModifiers::ALT,
111− "C" => KeyModifiers::CONTROL,
112− _ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
113− };
114−
115− if modifiers.contains(flag) {
116− return Err(anyhow!("Repeated key modifier '{}-'", token));
117− }
118− modifiers.insert(flag);
119− }
120−
121− Ok(KeyEvent { code, modifiers })
122− }
123−}
124−
125−impl<'de> Deserialize<'de> for KeyEvent {
126− fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127− where
128− D: Deserializer<'de>,
129− {
130− let s = String::deserialize(deserializer)?;
131− s.parse().map_err(de::Error::custom)
132− }
133−}
134−
135−impl From<event::KeyEvent> for KeyEvent {
136− fn from(event::KeyEvent { code, modifiers }: event::KeyEvent) -> KeyEvent {
137− KeyEvent { code, modifiers }
138− }
139−}
140−
141−#[cfg(test)]
142−mod test {
143− use super::*;
144−
145− #[test]
146− fn parsing_unmodified_keys() {
147− assert_eq!(
148− str::parse::<KeyEvent>("backspace").unwrap(),
149− KeyEvent {
150− code: KeyCode::Backspace,
151− modifiers: KeyModifiers::NONE
152− }
153− );
154−
155− assert_eq!(
156− str::parse::<KeyEvent>("left").unwrap(),
157− KeyEvent {
158− code: KeyCode::Left,
159− modifiers: KeyModifiers::NONE
160− }
161− );
162−
163− assert_eq!(
164− str::parse::<KeyEvent>(",").unwrap(),
165− KeyEvent {
166− code: KeyCode::Char(','),
167− modifiers: KeyModifiers::NONE
168− }
169− );
170−
171− assert_eq!(
172− str::parse::<KeyEvent>("w").unwrap(),
173− KeyEvent {
174− code: KeyCode::Char('w'),
175− modifiers: KeyModifiers::NONE
176− }
177− );
178−
179− assert_eq!(
180− str::parse::<KeyEvent>("F12").unwrap(),
181− KeyEvent {
182− code: KeyCode::F(12),
183− modifiers: KeyModifiers::NONE
184− }
185− );
186− }
187−
188− #[test]
189− fn parsing_modified_keys() {
190− assert_eq!(
191− str::parse::<KeyEvent>("S-minus").unwrap(),
192− KeyEvent {
193− code: KeyCode::Char('-'),
194− modifiers: KeyModifiers::SHIFT
195− }
196− );
197−
198− assert_eq!(
199− str::parse::<KeyEvent>("C-A-S-F12").unwrap(),
200− KeyEvent {
201− code: KeyCode::F(12),
202− modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
203− }
204− );
205−
206− assert_eq!(
207− str::parse::<KeyEvent>("S-C-2").unwrap(),
208− KeyEvent {
209− code: KeyCode::Char('2'),
210− modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
211− }
212− );
213− }
214−
215− #[test]
216− fn parsing_nonsensical_keys_fails() {
217− assert!(str::parse::<KeyEvent>("F13").is_err());
218− assert!(str::parse::<KeyEvent>("F0").is_err());
219− assert!(str::parse::<KeyEvent>("aaa").is_err());
220− assert!(str::parse::<KeyEvent>("S-S-a").is_err());
221− assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err());
222− assert!(str::parse::<KeyEvent>("FU").is_err());
223− assert!(str::parse::<KeyEvent>("123").is_err());
224− assert!(str::parse::<KeyEvent>("S--").is_err());
225− }
226−}
helix-view/src/lib.rs+4−5
@@ -1,18 +1,17 @@
11#[macro_use]
22pub mod macros;
33
4+pub mod clipboard;
45pub mod document;
56pub mod editor;
6−pub mod input;
77pub mod register_selection;
88pub mod theme;
99pub mod tree;
1010pub mod view;
1111
12−slotmap::new_key_type! {
13− pub struct DocumentId;
14− pub struct ViewId;
15−}
12+use slotmap::new_key_type;
13+new_key_type! { pub struct DocumentId; }
14+new_key_type! { pub struct ViewId; }
1615
1716pub use document::Document;
1817pub use editor::Editor;
helix-view/src/theme.rs+84−2
@@ -1,6 +1,11 @@
1−use std::collections::HashMap;
1+use std::{
2+ collections::HashMap,
3+ path::{Path, PathBuf},
4+};
25
6+use anyhow::Context;
37use log::warn;
8+use once_cell::sync::Lazy;
49use serde::{Deserialize, Deserializer};
510use toml::Value;
611
@@ -86,7 +91,84 @@ pub use tui::style::{Color, Modifier, Style};
8691// }
8792
8893/// Color theme for syntax highlighting.
89−#[derive(Debug)]
94+
95+pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
96+ toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
97+});
98+
99+#[derive(Clone, Debug)]
100+pub struct Loader {
101+ user_dir: PathBuf,
102+ default_dir: PathBuf,
103+}
104+impl Loader {
105+ /// Creates a new loader that can load themes from two directories.
106+ pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
107+ Self {
108+ user_dir: user_dir.as_ref().join("themes"),
109+ default_dir: default_dir.as_ref().join("themes"),
110+ }
111+ }
112+
113+ /// Loads a theme first looking in the `user_dir` then in `default_dir`
114+ pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
115+ if name == "default" {
116+ return Ok(self.default());
117+ }
118+ let filename = format!("{}.toml", name);
119+
120+ let user_path = self.user_dir.join(&filename);
121+ let path = if user_path.exists() {
122+ user_path
123+ } else {
124+ self.default_dir.join(filename)
125+ };
126+
127+ let data = std::fs::read(&path)?;
128+ toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
129+ }
130+
131+ pub fn read_names(path: &Path) -> Vec<String> {
132+ std::fs::read_dir(path)
133+ .map(|entries| {
134+ entries
135+ .filter_map(|entry| {
136+ if let Ok(entry) = entry {
137+ let path = entry.path();
138+ if let Some(ext) = path.extension() {
139+ if ext != "toml" {
140+ return None;
141+ }
142+ return Some(
143+ entry
144+ .file_name()
145+ .to_string_lossy()
146+ .trim_end_matches(".toml")
147+ .to_owned(),
148+ );
149+ }
150+ }
151+ None
152+ })
153+ .collect()
154+ })
155+ .unwrap_or_default()
156+ }
157+
158+ /// Lists all theme names available in default and user directory
159+ pub fn names(&self) -> Vec<String> {
160+ let mut names = Self::read_names(&self.user_dir);
161+ names.extend(Self::read_names(&self.default_dir));
162+ names
163+ }
164+
165+ /// Returns the default theme
166+ pub fn default(&self) -> Theme {
167+ DEFAULT_THEME.clone()
168+ }
169+}
170+
171+#[derive(Clone, Debug)]
90172pub struct Theme {
91173 scopes: Vec<String>,
92174 styles: HashMap<String, Style>,
helix-view/src/tree.rs+4
@@ -434,6 +434,10 @@ impl Tree {
434434 self.focus = key;
435435 }
436436 }
437+
438+ pub fn area(&self) -> Rect {
439+ self.area
440+ }
437441}
438442
439443#[derive(Debug)]
contrib/themes/README.md → runtime/themes/README.mdrenamed

The content is the same.

contrib/themes/bogster.toml → runtime/themes/bogster.tomlrenamed

The content is the same.

contrib/themes/ingrid.toml → runtime/themes/ingrid.tomlrenamed

The content is the same.

contrib/themes/onedark.toml → runtime/themes/onedark.tomlrenamed

The content is the same.