on master help sign in
Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection
e686c3eNathan Vegdahl
.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:
68 uses: actions-rs/cargo@v1
68 uses: actions-rs/cargo@v1
69 with:
69 with:
70 command: test
70 command: test
71− args: --locked
71+ args: --release --locked
72
72
73 - name: Build release binary
73 - name: Build release binary
74 uses: actions-rs/cargo@v1
74 uses: actions-rs/cargo@v1
Cargo.lock+24
@@ -17,6 +17,12 @@ version = "1.0.41"
17source = "registry+https://github.com/rust-lang/crates.io-index"
17source = "registry+https://github.com/rust-lang/crates.io-index"
18checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
18checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
19
19
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+
20[[package]]
26[[package]]
21name = "autocfg"
27name = "autocfg"
22version = "1.0.1"
28version = "1.0.1"
@@ -134,6 +140,12 @@ dependencies = [
134 "winapi",
140 "winapi",
135]
141]
136
142
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+
137[[package]]
149[[package]]
138name = "etcetera"
150name = "etcetera"
139version = "0.3.2"
151version = "0.3.2"
@@ -254,6 +266,7 @@ dependencies = [
254name = "helix-core"
266name = "helix-core"
255version = "0.2.0"
267version = "0.2.0"
256dependencies = [
268dependencies = [
269+ "arc-swap",
257 "etcetera",
270 "etcetera",
258 "helix-syntax",
271 "helix-syntax",
259 "once_cell",
272 "once_cell",
@@ -354,6 +367,7 @@ dependencies = [
354 "tokio",
367 "tokio",
355 "toml",
368 "toml",
356 "url",
369 "url",
370+ "which",
357]
371]
358
372
359[[package]]
373[[package]]
@@ -1057,6 +1071,16 @@ version = "0.10.2+wasi-snapshot-preview1"
1057source = "registry+https://github.com/rust-lang/crates.io-index"
1071source = "registry+https://github.com/rust-lang/crates.io-index"
1058checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
1072checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
1059
1073
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+
1060[[package]]
1084[[package]]
1061name = "winapi"
1085name = "winapi"
1062version = "0.3.9"
1086version = "0.3.9"
book/src/SUMMARY.md+1
@@ -3,6 +3,7 @@
3- [Installation](./install.md)
3- [Installation](./install.md)
4- [Usage](./usage.md)
4- [Usage](./usage.md)
5- [Configuration](./configuration.md)
5- [Configuration](./configuration.md)
6+ - [Themes](./themes.md)
6 - [Keymap](./keymap.md)
7 - [Keymap](./keymap.md)
7 - [Key Remapping](./remapping.md)
8 - [Key Remapping](./remapping.md)
8 - [Hooks](./hooks.md)
9 - [Hooks](./hooks.md)
book/src/configuration.md+2−89
@@ -1,97 +1,10 @@
1# Configuration
1# Configuration
2
2
3+To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
4+
3## LSP
5## LSP
4
6
5To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
7To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
6```toml
8```toml
7lsp-progress = false
9lsp-progress = false
8```
10```
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 @@
69| `;` | Collapse selection onto a single cursor |
69| `;` | Collapse selection onto a single cursor |
70| `Alt-;` | Flip selection cursor and anchor |
70| `Alt-;` | Flip selection cursor and anchor |
71| `%` | Select entire file |
71| `%` | Select entire file |
72−| `x` | Select current line |
72+| `x` | Select current line, if already selected, extend to next line |
73−| `X` | Extend to next line |
74−| `[` | Expand selection to parent syntax node TODO: pick a key |
73+| `` | Expand selection to parent syntax node TODO: pick a key |
75| `J` | join lines inside selection |
74| `J` | join lines inside selection |
76| `K` | keep selections matching the regex TODO: overlapped by hover help |
75| `K` | keep selections matching the regex TODO: overlapped by hover help |
77| `Space` | keep only the primary selection TODO: overlapped by space mode |
76| `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.
155
154
156| Key | Description |
155| Key | Description |
157| ----- | ------------- |
156| ----- | ------------- |
158−| `w`, `ctrl-w` | Switch to next window |
157+| `w`, `Ctrl-w` | Switch to next window |
159−| `v`, `ctrl-v` | Vertical right split |
158+| `v`, `Ctrl-v` | Vertical right split |
160−| `h`, `ctrl-h` | Horizontal bottom split |
159+| `h`, `Ctrl-h` | Horizontal bottom split |
161−| `q`, `ctrl-q` | Close current window |
160+| `q`, `Ctrl-q` | Close current window |
162
161
163## Space mode
162## Space mode
164
163
@@ -171,6 +170,11 @@ This layer is a kludge of mappings I had under leader key in neovim.
171| `s` | Open symbol picker (current document) |
170| `s` | Open symbol picker (current document) |
172| `w` | Enter [window mode](#window-mode) |
171| `w` | Enter [window mode](#window-mode) |
173| `space` | Keep primary selection TODO: it's here because space mode replaced it |
172| `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 |
174
178
175# Picker
179# Picker
176
180
@@ -184,4 +188,4 @@ Keys to use within picker.
184| `Enter` | Open selected |
188| `Enter` | Open selected |
185| `Ctrl-h` | Open horizontally |
189| `Ctrl-h` | Open horizontally |
186| `Ctrl-v` | Open vertically |
190| `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
22Control, Shift and Alt modifiers are encoded respectively with the prefixes
22Control, Shift and Alt modifiers are encoded respectively with the prefixes
23`C-`, `S-` and `A-`. Special keys are encoded as follows:
23`C-`, `S-` and `A-`. Special keys are encoded as follows:
24
24
25+| Key name | Representation |
26+| --- | --- |
25−* Backspace => "backspace"
27+| Backspace | `"backspace"` |
26−* Space => "space"
28+| Space | `"space"` |
27−* Return/Enter => "ret"
29+| Return/Enter | `"ret"` |
28−* < => "lt"
30+| < | `"lt"` |
29−* \> => "gt"
31+| \> | `"gt"` |
30−* \+ => "plus"
32+| \+ | `"plus"` |
31−* \- => "minus"
33+| \- | `"minus"` |
32−* ; => "semicolon"
34+| ; | `"semicolon"` |
33−* % => "percent"
35+| % | `"percent"` |
34−* Left => "left"
36+| Left | `"left"` |
35−* Right => "right"
37+| Right | `"right"` |
36−* Up => "up"
38+| Up | `"up"` |
37−* Home => "home"
39+| Home | `"home"` |
38−* End => "end"
40+| End | `"end"` |
39−* Page Up => "pageup"
41+| Page | `"pageup"` |
40−* Page Down => "pagedown"
42+| Page | `"pagedown"` |
41−* Tab => "tab"
43+| Tab | `"tab"` |
42−* Back Tab => "backtab"
44+| Back | `"backtab"` |
43−* Delete => "del"
45+| Delete | `"del"` |
44−* Insert => "ins"
46+| Insert | `"ins"` |
45−* Null => "null"
47+| Null | `"null"` |
46−* Escape => "esc"
48+| Escape | `"esc"` |
47
49
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" }
19ropey = "1.3"
19ropey = "1.3"
20smallvec = "1.4"
20smallvec = "1.4"
21tendril = "0.4.2"
21tendril = "0.4.2"
22−unicode-segmentation = "1.7.1"
22+unicode-segmentation = "1.7"
23unicode-width = "0.1"
23unicode-width = "0.1"
24−unicode-general-category = "0.4.0"
24+unicode-general-category = "0.4"
25# slab = "0.4.2"
25# slab = "0.4.2"
26tree-sitter = "0.19"
26tree-sitter = "0.19"
27once_cell = "1.8"
27once_cell = "1.8"
28+arc-swap = "1"
28regex = "1"
29regex = "1"
29
30
30serde = { version = "1.0", features = ["derive"] }
31serde = { version = "1.0", features = ["derive"] }
helix-core/src/indent.rs+17−20
@@ -254,26 +254,23 @@ where
254 Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
254 Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
255 };
255 };
256 use once_cell::sync::OnceCell;
256 use once_cell::sync::OnceCell;
257− let loader = Loader::new(
257+ let loader = Loader::new(Configuration {
258− Configuration {
259− language: vec![LanguageConfiguration {
258+ language: vec![LanguageConfiguration {
260− scope: "source.rust".to_string(),
259+ scope: "source.rust".to_string(),
261− file_types: vec!["rs".to_string()],
260+ file_types: vec!["rs".to_string()],
262− language_id: Lang::Rust,
261+ language_id: Lang::Rust,
263− highlight_config: OnceCell::new(),
262+ highlight_config: OnceCell::new(),
264− //
263+ //
265− roots: vec![],
264+ roots: vec![],
266− auto_format: false,
265+ auto_format: false,
267− language_server: None,
266+ language_server: None,
268− indent: Some(IndentationConfiguration {
267+ indent: Some(IndentationConfiguration {
269− tab_width: 4,
268+ tab_width: 4,
270− unit: String::from(" "),
269+ unit: String::from(" "),
271− }),
270+ }),
272− indent_query: OnceCell::new(),
271+ indent_query: OnceCell::new(),
273− }],
274− },
272+ }],
275− Vec::new(),
276− );
273+ });
277
274
278 // set runtime path so we can find the queries
275 // set runtime path so we can find the queries
279 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
276 let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
helix-core/src/lib.rs+7−3
@@ -19,6 +19,12 @@ mod state;
19pub mod syntax;
19pub mod syntax;
20mod transaction;
20mod transaction;
21
21
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+
22static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
28static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
23 once_cell::sync::Lazy::new(runtime_dir);
29 once_cell::sync::Lazy::new(runtime_dir);
24
30
@@ -51,7 +57,7 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
51}
57}
52
58
53#[cfg(not(embed_runtime))]
59#[cfg(not(embed_runtime))]
54−fn runtime_dir() -> std::path::PathBuf {
60+pub fn runtime_dir() -> std::path::PathBuf {
55 if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
61 if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
56 return dir.into();
62 return dir.into();
57 }
63 }
@@ -98,8 +104,6 @@ pub use ropey::{Rope, RopeSlice};
98
104
99pub use tendril::StrTendril as Tendril;
105pub use tendril::StrTendril as Tendril;
100
106
101−pub use unicode_general_category::get_general_category;
102−
103#[doc(inline)]
107#[doc(inline)]
104pub use {regex, tree_sitter};
108pub use {regex, tree_sitter};
105
109
helix-core/src/syntax.rs+57−41
@@ -1,6 +1,8 @@
1use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
1use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
2pub use helix_syntax::{get_language, get_language_name, Lang};
2pub use helix_syntax::{get_language, get_language_name, Lang};
3
3
4+use arc_swap::ArcSwap;
5+
4use std::{
6use std::{
5 borrow::Cow,
7 borrow::Cow,
6 cell::RefCell,
8 cell::RefCell,
@@ -143,37 +145,49 @@ fn read_query(language: &str, filename: &str) -> String {
143}
145}
144
146
145impl LanguageConfiguration {
147impl LanguageConfiguration {
146− pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
148+ fn initialize_highlight(&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();
149+ let language = get_language_name(self.language_id).to_ascii_lowercase();
150
150
151− let highlights_query = read_query(&language, "highlights.scm");
151+ let highlights_query = read_query(&language, "highlights.scm");
152− // always highlight syntax errors
152+ // always highlight syntax errors
153− // highlights_query += "\n(ERROR) @error";
153+ // highlights_query += "\n(ERROR) @error";
154
154
155− let injections_query = read_query(&language, "injections.scm");
155+ let injections_query = read_query(&language, "injections.scm");
156
156
157− let locals_query = "";
157+ let locals_query = "";
158
158
159− if highlights_query.is_empty() {
159+ if highlights_query.is_empty() {
160− None
160+ None
161− } else {
161+ } else {
162− let language = get_language(self.language_id);
162+ let language = get_language(self.language_id);
163− let mut config = HighlightConfiguration::new(
163+ let mut config = HighlightConfiguration::new(
164− language,
164+ language,
165− &highlights_query,
165+ &highlights_query,
166− &injections_query,
166+ &injections_query,
167− locals_query,
167+ locals_query,
168− )
168+ )
169− .unwrap(); // TODO: no unwrap
169+ .unwrap(); // TODO: no unwrap
170− config.configure(scopes);
170+ config.configure(scopes);
171− Some(Arc::new(config))
171+ Some(Arc::new(config))
172− }
172+ }
173+ }
174+
175+ pub fn reconfigure(&self, scopes: &[String]) {
176+ if let Some(Some(config)) = self.highlight_config.get() {
177+ config.configure(scopes);
173− })
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))
174 .clone()
184 .clone()
175 }
185 }
176
186
187+ pub fn is_highlight_initialized(&self) -> bool {
188+ self.highlight_config.get().is_some()
189+ }
190+
177 pub fn indent_query(&self) -> Option<&IndentQuery> {
191 pub fn indent_query(&self) -> Option<&IndentQuery> {
178 self.indent_query
192 self.indent_query
179 .get_or_init(|| {
193 .get_or_init(|| {
@@ -190,22 +204,18 @@ impl LanguageConfiguration {
190 }
204 }
191}
205}
192
206
193−pub static LOADER: OnceCell<Loader> = OnceCell::new();
194−
195#[derive(Debug)]
207#[derive(Debug)]
196pub struct Loader {
208pub struct Loader {
197 // highlight_names ?
209 // highlight_names ?
198 language_configs: Vec<Arc<LanguageConfiguration>>,
210 language_configs: Vec<Arc<LanguageConfiguration>>,
199 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
211 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
200− scopes: Vec<String>,
201}
212}
202
213
203impl Loader {
214impl Loader {
204− pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
215+ pub fn new(config: Configuration) -> Self {
205 let mut loader = Self {
216 let mut loader = Self {
206 language_configs: Vec::new(),
217 language_configs: Vec::new(),
207 language_config_ids_by_file_type: HashMap::new(),
218 language_config_ids_by_file_type: HashMap::new(),
208− scopes,
209 };
219 };
210
220
211 for config in config.language {
221 for config in config.language {
@@ -225,10 +235,6 @@ impl Loader {
225 loader
235 loader
226 }
236 }
227
237
228− pub fn scopes(&self) -> &[String] {
229− &self.scopes
230− }
231−
232 pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
238 pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
233 // Find all the language configurations that match this file name
239 // Find all the language configurations that match this file name
234 // or a suffix of the file name.
240 // or a suffix of the file name.
@@ -253,6 +259,10 @@ impl Loader {
253 .find(|config| config.scope == scope)
259 .find(|config| config.scope == scope)
254 .cloned()
260 .cloned()
255 }
261 }
262+
263+ pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
264+ self.language_configs.iter()
265+ }
256}
266}
257
267
258pub struct TsParser {
268pub struct TsParser {
@@ -772,7 +782,7 @@ pub struct HighlightConfiguration {
772 combined_injections_query: Option<Query>,
782 combined_injections_query: Option<Query>,
773 locals_pattern_index: usize,
783 locals_pattern_index: usize,
774 highlights_pattern_index: usize,
784 highlights_pattern_index: usize,
775− highlight_indices: Vec<Option<Highlight>>,
785+ highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
776 non_local_variable_patterns: Vec<bool>,
786 non_local_variable_patterns: Vec<bool>,
777 injection_content_capture_index: Option<u32>,
787 injection_content_capture_index: Option<u32>,
778 injection_language_capture_index: Option<u32>,
788 injection_language_capture_index: Option<u32>,
@@ -924,7 +934,7 @@ impl HighlightConfiguration {
924 }
934 }
925 }
935 }
926
936
927− let highlight_indices = vec![None; query.capture_names().len()];
937+ let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
928 Ok(Self {
938 Ok(Self {
929 language,
939 language,
930 query,
940 query,
@@ -957,17 +967,20 @@ impl HighlightConfiguration {
957 ///
967 ///
958 /// When highlighting, results are returned as `Highlight` values, which contain the index
968 /// When highlighting, results are returned as `Highlight` values, which contain the index
959 /// of the matched highlight this list of highlight names.
969 /// 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]) {
961 let mut capture_parts = Vec::new();
971 let mut capture_parts = Vec::new();
962− self.highlight_indices.clear();
972+ let indices: Vec<_> = self
963− self.highlight_indices
973+ .query
974+ .capture_names()
975+ .iter()
964− .extend(self.query.capture_names().iter().map(move |capture_name| {
976+ .map(move |capture_name| {
965 capture_parts.clear();
977 capture_parts.clear();
966 capture_parts.extend(capture_name.split('.'));
978 capture_parts.extend(capture_name.split('.'));
967
979
968 let mut best_index = None;
980 let mut best_index = None;
969 let mut best_match_len = 0;
981 let mut best_match_len = 0;
970 for (i, recognized_name) in recognized_names.iter().enumerate() {
982 for (i, recognized_name) in recognized_names.iter().enumerate() {
983+ let recognized_name = recognized_name;
971 let mut len = 0;
984 let mut len = 0;
972 let mut matches = true;
985 let mut matches = true;
973 for part in recognized_name.split('.') {
986 for part in recognized_name.split('.') {
@@ -983,7 +996,10 @@ impl HighlightConfiguration {
983 }
996 }
984 }
997 }
985 best_index.map(Highlight)
998 best_index.map(Highlight)
986− }));
999+ })
1000+ .collect();
1001+
1002+ self.highlight_indices.store(Arc::new(indices));
987 }
1003 }
988}
1004}
989
1005
@@ -1562,7 +1578,7 @@ where
1562 }
1578 }
1563 }
1579 }
1564
1580
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];
1566
1582
1567 // If this node represents a local definition, then store the current
1583 // If this node represents a local definition, then store the current
1568 // highlight value on the local scope entry representing this node.
1584 // 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;
1use helix_lsp::{lsp, LspProgressMap};
2use 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};
3
4
4−use crate::{args::Args, compositor::Compositor, config::Config, ui};
5+use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
5
6
6use log::{error, info};
7use log::{error, info};
7
8
@@ -14,7 +15,7 @@ use std::{
14 time::Duration,
15 time::Duration,
15};
16};
16
17
17−use anyhow::Error;
18+use anyhow::{Context, Error};
18
19
19use crossterm::{
20use crossterm::{
20 event::{Event, EventStream},
21 event::{Event, EventStream},
@@ -36,6 +37,8 @@ pub struct Application {
36 compositor: Compositor,
37 compositor: Compositor,
37 editor: Editor,
38 editor: Editor,
38
39
40+ theme_loader: Arc<theme::Loader>,
41+ syn_loader: Arc<syntax::Loader>,
39 callbacks: LspCallbacks,
42 callbacks: LspCallbacks,
40
43
41 lsp_progress: LspProgressMap,
44 lsp_progress: LspProgressMap,
@@ -47,9 +50,36 @@ impl Application {
47 use helix_view::editor::Action;
50 use helix_view::editor::Action;
48 let mut compositor = Compositor::new()?;
51 let mut compositor = Compositor::new()?;
49 let size = compositor.size();
52 let size = compositor.size();
50− let mut editor = Editor::new(size);
51
53
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+
52− let mut editor_view = Box::new(ui::EditorView::new(config.keys));
82+ let mut editor_view = Box::new(ui::EditorView::new(config.keymaps));
53 compositor.push(editor_view);
83 compositor.push(editor_view);
54
84
55 if !args.files.is_empty() {
85 if !args.files.is_empty() {
@@ -72,10 +102,14 @@ impl Application {
72 editor.new_file(Action::VerticalSplit);
102 editor.new_file(Action::VerticalSplit);
73 }
103 }
74
104
105+ editor.set_theme(theme);
106+
75 let mut app = Self {
107 let mut app = Self {
76 compositor,
108 compositor,
77 editor,
109 editor,
78
110
111+ theme_loader,
112+ syn_loader,
79 callbacks: FuturesUnordered::new(),
113 callbacks: FuturesUnordered::new(),
80 lsp_progress: LspProgressMap::new(),
114 lsp_progress: LspProgressMap::new(),
81 lsp_progress_enabled: config.global.lsp_progress,
115 lsp_progress_enabled: config.global.lsp_progress,
helix-term/src/commands.rs+232−62
@@ -11,7 +11,6 @@ use helix_core::{
11
11
12use helix_view::{
12use helix_view::{
13 document::{IndentStyle, Mode},
13 document::{IndentStyle, Mode},
14− input::{KeyCode, KeyEvent},
15 view::{View, PADDING},
14 view::{View, PADDING},
16 Document, DocumentId, Editor, ViewId,
15 Document, DocumentId, Editor, ViewId,
17};
16};
@@ -39,8 +38,8 @@ use std::{
39 path::{Path, PathBuf},
38 path::{Path, PathBuf},
40};
39};
41
40
41+use crossterm::event::{KeyCode, KeyEvent};
42use once_cell::sync::Lazy;
42use once_cell::sync::Lazy;
43−use serde::de::{self, Deserialize, Deserializer};
44
43
45pub struct Context<'a> {
44pub struct Context<'a> {
46 pub selected_register: helix_view::RegisterSelection,
45 pub selected_register: helix_view::RegisterSelection,
@@ -186,7 +185,6 @@ impl Command {
186 search_next,
185 search_next,
187 extend_search_next,
186 extend_search_next,
188 search_selection,
187 search_selection,
189− select_line,
190 extend_line,
188 extend_line,
191 delete_selection,
189 delete_selection,
192 change_selection,
190 change_selection,
@@ -223,9 +221,14 @@ impl Command {
223 undo,
221 undo,
224 redo,
222 redo,
225 yank,
223 yank,
224+ yank_joined_to_clipboard,
225+ yank_main_selection_to_clipboard,
226 replace_with_yanked,
226 replace_with_yanked,
227+ replace_selections_with_clipboard,
227 paste_after,
228 paste_after,
228 paste_before,
229 paste_before,
230+ paste_clipboard_after,
231+ paste_clipboard_before,
229 indent,
232 indent,
230 unindent,
233 unindent,
231 format_selections,
234 format_selections,
@@ -253,48 +256,6 @@ impl Command {
253 );
256 );
254}
257}
255
258
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−
298fn move_char_left(cx: &mut Context) {
259fn move_char_left(cx: &mut Context) {
299 let count = cx.count();
260 let count = cx.count();
300 let (view, doc) = current!(cx.editor);
261 let (view, doc) = current!(cx.editor);
@@ -926,21 +887,6 @@ fn search_selection(cx: &mut Context) {
926
887
927//
888//
928
889
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−}
944fn extend_line(cx: &mut Context) {
890fn extend_line(cx: &mut Context) {
945 let count = cx.count();
891 let count = cx.count();
946 let (view, doc) = current!(cx.editor);
892 let (view, doc) = current!(cx.editor);
@@ -1318,6 +1264,57 @@ mod cmd {
1318 quit_all_impl(editor, args, event, true)
1264 quit_all_impl(editor, args, event, true)
1319 }
1265 }
1320
1266
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+
1321 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
1318 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
1322 TypableCommand {
1319 TypableCommand {
1323 name: "quit",
1320 name: "quit",
@@ -1431,7 +1428,55 @@ mod cmd {
1431 fun: force_quit_all,
1428 fun: force_quit_all,
1432 completer: None,
1429 completer: None,
1433 },
1430 },
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+ },
1435 ];
1480 ];
1436
1481
1437 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
1482 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2424,6 +2469,52 @@ fn yank(cx: &mut Context) {
2424 cx.editor.set_status(msg)
2469 cx.editor.set_status(msg)
2425}
2470}
2426
2471
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+
2427#[derive(Copy, Clone)]
2518#[derive(Copy, Clone)]
2428enum Paste {
2519enum Paste {
2429 Before,
2520 Before,
@@ -2469,6 +2560,31 @@ fn paste_impl(
2469 Some(transaction)
2560 Some(transaction)
2470}
2561}
2471
2562
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+
2472fn replace_with_yanked(cx: &mut Context) {
2588fn replace_with_yanked(cx: &mut Context) {
2473 let reg_name = cx.selected_register.name();
2589 let reg_name = cx.selected_register.name();
2474 let (view, doc) = current!(cx.editor);
2590 let (view, doc) = current!(cx.editor);
@@ -2489,6 +2605,29 @@ fn replace_with_yanked(cx: &mut Context) {
2489 }
2605 }
2490}
2606}
2491
2607
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+
2492// alt-p => paste every yanked selection after selected text
2631// alt-p => paste every yanked selection after selected text
2493// alt-P => paste every yanked selection before selected text
2632// alt-P => paste every yanked selection before selected text
2494// R => replace selected text with yanked text
2633// R => replace selected text with yanked text
@@ -2854,7 +2993,7 @@ fn hover(cx: &mut Context) {
2854
2993
2855 // skip if contents empty
2994 // skip if contents empty
2856
2995
2857− let contents = ui::Markdown::new(contents);
2996+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
2858 let mut popup = Popup::new(contents);
2997 let mut popup = Popup::new(contents);
2859 compositor.push(Box::new(popup));
2998 compositor.push(Box::new(popup));
2860 }
2999 }
@@ -3009,6 +3148,11 @@ fn space_mode(cx: &mut Context) {
3009 'b' => buffer_picker(cx),
3148 'b' => buffer_picker(cx),
3010 's' => symbol_picker(cx),
3149 's' => symbol_picker(cx),
3011 'w' => window_mode(cx),
3150 '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),
3012 // ' ' => toggle_alternate_buffer(cx),
3156 // ' ' => toggle_alternate_buffer(cx),
3013 // TODO: temporary since space mode took its old key
3157 // TODO: temporary since space mode took its old key
3014 ' ' => keep_primary_selection(cx),
3158 ' ' => keep_primary_selection(cx),
@@ -3092,3 +3236,29 @@ fn right_bracket_mode(cx: &mut Context) {
3092 }
3236 }
3093 })
3237 })
3094}
3238}
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 {
178 /// Returns a boxed any from a boxed self.
178 /// Returns a boxed any from a boxed self.
179 ///
179 ///
180 /// Can be used before `Box::downcast()`.
180 /// Can be used before `Box::downcast()`.
181− ///
181+ //
182− /// # Examples
182+ // # Examples
183− ///
183+ //
184− /// ```rust
184+ // ```rust
185− /// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
185+ // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
186− /// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
186+ // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
187− /// ```
187+ // ```
188 fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
188 fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
189}
189}
190
190
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};
2
3
3−use crate::commands::Command;
4+use serde::{de::Error as SerdeError, Deserialize, Serialize};
5+
4−use crate::keymap::Keymaps;
6+use crate::keymap::{parse_keymaps, Keymaps};
5
7
6−#[derive(Debug, PartialEq, Deserialize)]
7pub struct GlobalConfig {
8pub struct GlobalConfig {
9+ pub theme: Option<String>,
8 pub lsp_progress: bool,
10 pub lsp_progress: bool,
9}
11}
10
12
11impl Default for GlobalConfig {
13impl Default for GlobalConfig {
12 fn default() -> Self {
14 fn default() -> Self {
15+ Self {
13− Self { lsp_progress: true }
16+ lsp_progress: true,
17+ theme: None,
18+ }
14 }
19 }
15}
20}
16
21
17−#[derive(Debug, Default, PartialEq, Deserialize)]
22+#[derive(Default)]
18−#[serde(default)]
19pub struct Config {
23pub struct Config {
20 pub global: GlobalConfig,
24 pub global: GlobalConfig,
21− pub keys: Keymaps,
25+ pub keymaps: Keymaps,
22}
26}
23
27
24−#[test]
28+#[derive(Serialize, Deserialize)]
25−fn parsing_keymaps_config_file() {
29+#[serde(rename_all = "kebab-case")]
26− use helix_core::hashmap;
30+struct TomlConfig {
27− use helix_view::document::Mode;
31+ theme: Option<String>,
28− use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
32+ lsp_progress: Option<bool>,
29−
33+ keys: Option<HashMap<String, HashMap<String, String>>>,
30− let sample_keymaps = r#"
34+}
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− "#;
38
35
39− assert_eq!(
36+impl<'de> Deserialize<'de> for Config {
40− toml::from_str::<Config>(sample_keymaps).unwrap(),
37+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41− Config {
38+ where
42− global: Default::default(),
39+ D: serde::Deserializer<'de>,
43− keys: Keymaps(hashmap! {
40+ {
44− Mode::Insert => hashmap! {
41+ let config = TomlConfig::deserialize(deserializer)?;
45− KeyEvent {
42+ Ok(Self {
46− code: KeyCode::Char('y'),
43+ global: GlobalConfig {
47− modifiers: KeyModifiers::NONE,
44+ lsp_progress: config.lsp_progress.unwrap_or(true),
48− } => Command::move_line_down,
45+ theme: config.theme,
49− KeyEvent {
46+ },
50− code: KeyCode::Char('a'),
47+ keymaps: config
51− modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
48+ .keys
52− } => Command::delete_selection,
49+ .map(|r| parse_keymaps(&r))
53− },
50+ .transpose()
54− Mode::Normal => hashmap! {
51+ .map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))?
55− KeyEvent {
52+ .unwrap_or_else(Keymaps::default),
56− code: KeyCode::F(12),
57− modifiers: KeyModifiers::ALT,
58− } => Command::move_next_word_end,
59− },
53+ })
60− })
61− }
54+ }
62− );
63}
55}
helix-term/src/keymap.rs+319−23
@@ -3,8 +3,6 @@ pub use crate::commands::Command;
3use anyhow::{anyhow, Error, Result};
3use anyhow::{anyhow, Error, Result};
4use helix_core::hashmap;
4use helix_core::hashmap;
5use helix_view::document::Mode;
5use helix_view::document::Mode;
6−use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
7−use serde::Deserialize;
8use std::{
6use std::{
9 collections::HashMap,
7 collections::HashMap,
10 fmt::Display,
8 fmt::Display,
@@ -101,6 +99,14 @@ use std::{
101// D] = last diagnostic
99// D] = last diagnostic
102// }
100// }
103
101
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+
104#[macro_export]
110#[macro_export]
105macro_rules! key {
111macro_rules! key {
106 ($key:ident) => {
112 ($key:ident) => {
@@ -135,21 +141,9 @@ macro_rules! alt {
135 };
141 };
136}
142}
137
143
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−
150impl Default for Keymaps {
144impl Default for Keymaps {
151− fn default() -> Keymaps {
145+ fn default() -> Self {
152− let normal = hashmap!(
146+ let normal = Keymap(hashmap!(
153 key!('h') => Command::move_char_left,
147 key!('h') => Command::move_char_left,
154 key!('j') => Command::move_line_down,
148 key!('j') => Command::move_line_down,
155 key!('k') => Command::move_line_up,
149 key!('k') => Command::move_line_up,
@@ -202,9 +196,7 @@ impl Default for Keymaps {
202 key!(';') => Command::collapse_selection,
196 key!(';') => Command::collapse_selection,
203 alt!(';') => Command::flip_selections,
197 alt!(';') => Command::flip_selections,
204 key!('%') => Command::select_all,
198 key!('%') => Command::select_all,
205− key!('x') => Command::select_line,
199+ key!('x') => Command::extend_line,
206− key!('X') => Command::extend_line,
207− // or select mode X?
208 // extend_to_whole_line, crop_to_whole_line
200 // extend_to_whole_line, crop_to_whole_line
209
201
210
202
@@ -283,12 +275,12 @@ impl Default for Keymaps {
283 key!('z') => Command::view_mode,
275 key!('z') => Command::view_mode,
284
276
285 key!('"') => Command::select_register,
277 key!('"') => Command::select_register,
286− );
278+ ));
287 // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
279 // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
288 // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
280 // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
289 // because some selection operations can now be done from normal mode, some from select mode.
281 // because some selection operations can now be done from normal mode, some from select mode.
290 let mut select = normal.clone();
282 let mut select = normal.clone();
291− select.extend(
283+ select.0.extend(
292 hashmap!(
284 hashmap!(
293 key!('h') => Command::extend_char_left,
285 key!('h') => Command::extend_char_left,
294 key!('j') => Command::extend_line_down,
286 key!('j') => Command::extend_line_down,
@@ -321,7 +313,7 @@ impl Default for Keymaps {
321 // TODO: select could be normal mode with some bindings merged over
313 // TODO: select could be normal mode with some bindings merged over
322 Mode::Normal => normal,
314 Mode::Normal => normal,
323 Mode::Select => select,
315 Mode::Select => select,
324− Mode::Insert => hashmap!(
316+ Mode::Insert => Keymap(hashmap!(
325 key!(Esc) => Command::normal_mode as Command,
317 key!(Esc) => Command::normal_mode as Command,
326 key!(Backspace) => Command::delete_char_backward,
318 key!(Backspace) => Command::delete_char_backward,
327 key!(Delete) => Command::delete_char_forward,
319 key!(Delete) => Command::delete_char_forward,
@@ -333,9 +325,313 @@ impl Default for Keymaps {
333 key!(Right) => Command::move_char_right,
325 key!(Right) => Command::move_char_right,
334 key!(PageUp) => Command::page_up,
326 key!(PageUp) => Command::page_up,
335 key!(PageDown) => Command::page_down,
327 key!(PageDown) => Command::page_down,
328+ key!(Home) => Command::move_line_start,
329+ key!(End) => Command::move_line_end,
336 ctrl!('x') => Command::completion,
330 ctrl!('x') => Command::completion,
337 ctrl!('w') => Command::delete_word_backward,
331 ctrl!('w') => Command::delete_word_backward,
338− ),
332+ )),
339 ))
333 ))
340 }
334 }
341}
335}
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};
2use helix_term::application::Application;
1use helix_term::application::Application;
3use helix_term::args::Args;
2use helix_term::args::Args;
4use helix_term::config::Config;
3use helix_term::config::Config;
5use std::path::PathBuf;
4use std::path::PathBuf;
6
5
6+use anyhow::{Context, Result};
7+
7fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
8fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
8 let mut base_config = fern::Dispatch::new();
9 let mut base_config = fern::Dispatch::new();
9
10
@@ -88,11 +89,12 @@ FLAGS:
88 std::fs::create_dir_all(&conf_dir).ok();
89 std::fs::create_dir_all(&conf_dir).ok();
89 }
90 }
90
91
91− let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
92+ let config = std::fs::read_to_string(conf_dir.join("config.toml"))
93+ .ok()
92− Ok(config) => toml::from_str(&config)?,
94+ .map(|s| toml::from_str(&s))
93− Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
95+ .transpose()?
94− Err(err) => return Err(Error::new(err)),
96+ .or_else(|| Some(Config::default()))
95− };
97+ .unwrap();
96
98
97 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
99 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
98
100
helix-term/src/ui/completion.rs+40−19
@@ -238,6 +238,9 @@ impl Component for Completion {
238 .language()
238 .language()
239 .and_then(|scope| scope.strip_prefix("source."))
239 .and_then(|scope| scope.strip_prefix("source."))
240 .unwrap_or("");
240 .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;
241
244
242 let doc = match &option.documentation {
245 let doc = match &option.documentation {
243 Some(lsp::Documentation::String(contents))
246 Some(lsp::Documentation::String(contents))
@@ -246,42 +249,60 @@ impl Component for Completion {
246 value: contents,
249 value: contents,
247 })) => {
250 })) => {
248 // TODO: convert to wrapped text
251 // TODO: convert to wrapped text
249− Markdown::new(format!(
252+ Markdown::new(
253+ format!(
250− "```{}\n{}\n```\n{}",
254+ "```{}\n{}\n```\n{}",
251− language,
255+ language,
252− option.detail.as_deref().unwrap_or_default(),
256+ option.detail.as_deref().unwrap_or_default(),
253− contents.clone()
257+ contents.clone()
258+ ),
259+ cx.editor.syn_loader.clone(),
254− ))
260+ )
255 }
261 }
256 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
262 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
257 kind: lsp::MarkupKind::Markdown,
263 kind: lsp::MarkupKind::Markdown,
258 value: contents,
264 value: contents,
259 })) => {
265 })) => {
260 // TODO: set language based on doc scope
266 // TODO: set language based on doc scope
261− Markdown::new(format!(
267+ Markdown::new(
268+ format!(
262− "```{}\n{}\n```\n{}",
269+ "```{}\n{}\n```\n{}",
263− language,
270+ language,
264− option.detail.as_deref().unwrap_or_default(),
271+ option.detail.as_deref().unwrap_or_default(),
265− contents.clone()
272+ contents.clone()
273+ ),
274+ cx.editor.syn_loader.clone(),
266− ))
275+ )
267 }
276 }
268 None if option.detail.is_some() => {
277 None if option.detail.is_some() => {
269 // TODO: copied from above
278 // TODO: copied from above
270
279
271 // TODO: set language based on doc scope
280 // TODO: set language based on doc scope
272− Markdown::new(format!(
281+ Markdown::new(
282+ format!(
273− "```{}\n{}\n```",
283+ "```{}\n{}\n```",
274− language,
284+ language,
275− option.detail.as_deref().unwrap_or_default(),
285+ option.detail.as_deref().unwrap_or_default(),
286+ ),
287+ cx.editor.syn_loader.clone(),
276− ))
288+ )
277 }
289 }
278 None => return,
290 None => return,
279 };
291 };
280
292
281 let half = area.height / 2;
293 let half = area.height / 2;
282 let height = 15.min(half);
294 let height = 15.min(half);
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 {
283− // -2 to subtract command line + statusline. a bit of a hack, because of splits.
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+
284− let area = Rect::new(0, area.height - height - 2, area.width, height);
305+ let area = Rect::new(0, y, area.width, height);
285
306
286 // clear area
307 // clear area
287 let background = cx.editor.theme.get("ui.popup");
308 let background = cx.editor.theme.get("ui.popup");
helix-term/src/ui/editor.rs+5−7
@@ -11,13 +11,12 @@ use helix_core::{
11 syntax::{self, HighlightEvent},
11 syntax::{self, HighlightEvent},
12 LineEnding, Position, Range,
12 LineEnding, Position, Range,
13};
13};
14−use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
15use helix_view::{document::Mode, Document, Editor, Theme, View};
14use helix_view::{document::Mode, Document, Editor, Theme, View};
16use std::borrow::Cow;
15use std::borrow::Cow;
17
16
18use crossterm::{
17use crossterm::{
19 cursor,
18 cursor,
20− event::{read, Event, EventStream},
19+ event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
21};
20};
22use tui::{
21use tui::{
23 backend::CrosstermBackend,
22 backend::CrosstermBackend,
@@ -130,7 +129,7 @@ impl EditorView {
130 })],
129 })],
131 };
130 };
132 let mut spans = Vec::new();
131 let mut spans = Vec::new();
133− let mut visual_x = 0;
132+ let mut visual_x = 0u16;
134 let mut line = 0u16;
133 let mut line = 0u16;
135 let tab_width = doc.tab_width();
134 let tab_width = doc.tab_width();
136
135
@@ -186,7 +185,7 @@ impl EditorView {
186 break 'outer;
185 break 'outer;
187 }
186 }
188 } else if grapheme == "\t" {
187 } else if grapheme == "\t" {
189− visual_x += (tab_width as u16);
188+ visual_x = visual_x.saturating_add(tab_width as u16);
190 } else {
189 } else {
191 let out_of_bounds = visual_x < view.first_col as u16
190 let out_of_bounds = visual_x < view.first_col as u16
192 || visual_x >= viewport.width + view.first_col as u16;
191 || visual_x >= viewport.width + view.first_col as u16;
@@ -198,7 +197,7 @@ impl EditorView {
198
197
199 if out_of_bounds {
198 if out_of_bounds {
200 // if we're offscreen just keep going until we hit a new line
199 // 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);
202 continue;
201 continue;
203 }
202 }
204
203
@@ -608,8 +607,7 @@ impl Component for EditorView {
608 cx.editor.resize(Rect::new(0, 0, width, height - 1));
607 cx.editor.resize(Rect::new(0, 0, width, height - 1));
609 EventResult::Consumed(None)
608 EventResult::Consumed(None)
610 }
609 }
611− Event::Key(key) => {
610+ Event::Key(mut key) => {
612− let mut key = KeyEvent::from(key);
613 canonicalize_key(&mut key);
611 canonicalize_key(&mut key);
614 // clear status
612 // clear status
615 cx.editor.status_msg = None;
613 cx.editor.status_msg = None;
helix-term/src/ui/markdown.rs+18−14
@@ -7,25 +7,34 @@ use tui::{
7 text::Text,
7 text::Text,
8};
8};
9
9
10−use std::borrow::Cow;
10+use std::{borrow::Cow, sync::Arc};
11
11
12−use helix_core::Position;
12+use helix_core::{syntax, Position};
13use helix_view::{Editor, Theme};
13use helix_view::{Editor, Theme};
14
14
15pub struct Markdown {
15pub struct Markdown {
16 contents: String,
16 contents: String,
17+
18+ config_loader: Arc<syntax::Loader>,
17}
19}
18
20
19// TODO: pre-render and self reference via Pin
21// TODO: pre-render and self reference via Pin
20// better yet, just use Tendril + subtendril for references
22// better yet, just use Tendril + subtendril for references
21
23
22impl Markdown {
24impl Markdown {
23− pub fn new(contents: String) -> Self {
25+ pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
26+ Self {
24− Self { contents }
27+ contents,
28+ config_loader,
29+ }
25 }
30 }
26}
31}
27
32
33+fn parse<'a>(
34+ contents: &'a str,
28−fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
35+ theme: Option<&Theme>,
36+ loader: &syntax::Loader,
37+) -> tui::text::Text<'a> {
29 use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
38 use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
30 use tui::text::{Span, Spans, Text};
39 use tui::text::{Span, Spans, Text};
31
40
@@ -79,9 +88,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
79 use helix_core::Rope;
88 use helix_core::Rope;
80
89
81 let rope = Rope::from(text.as_ref());
90 let rope = Rope::from(text.as_ref());
82− let syntax = syntax::LOADER
91+ let syntax = loader
83− .get()
84− .unwrap()
85 .language_config_for_scope(&format!("source.{}", language))
92 .language_config_for_scope(&format!("source.{}", language))
86 .and_then(|config| config.highlight_config(theme.scopes()))
93 .and_then(|config| config.highlight_config(theme.scopes()))
87 .map(|config| Syntax::new(&rope, config));
94 .map(|config| Syntax::new(&rope, config));
@@ -101,9 +108,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
101 }
108 }
102 HighlightEvent::Source { start, end } => {
109 HighlightEvent::Source { start, end } => {
103 let style = match highlights.first() {
110 let style = match highlights.first() {
104− Some(span) => {
105− theme.get(theme.scopes()[span.0].as_str())
111+ Some(span) => theme.get(&theme.scopes()[span.0]),
106− }
107 None => text_style,
112 None => text_style,
108 };
113 };
109
114
@@ -159,7 +164,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
159 }
164 }
160 }
165 }
161 Event::Code(text) | Event::Html(text) => {
166 Event::Code(text) | Event::Html(text) => {
162− log::warn!("code {:?}", text);
163 let mut span = to_span(text);
167 let mut span = to_span(text);
164 span.style = code_style;
168 span.style = code_style;
165 spans.push(span);
169 spans.push(span);
@@ -198,7 +202,7 @@ impl Component for Markdown {
198 fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
202 fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
199 use tui::widgets::{Paragraph, Widget, Wrap};
203 use tui::widgets::{Paragraph, Widget, Wrap};
200
204
201− let text = parse(&self.contents, Some(&cx.editor.theme));
205+ let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
202
206
203 let par = Paragraph::new(text)
207 let par = Paragraph::new(text)
204 .wrap(Wrap { trim: false })
208 .wrap(Wrap { trim: false })
@@ -209,7 +213,7 @@ impl Component for Markdown {
209 }
213 }
210
214
211 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
215 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);
213 let padding = 2;
217 let padding = 2;
214 let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
218 let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
215 let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
219 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> {
115
115
116pub mod completers {
116pub mod completers {
117 use crate::ui::prompt::Completion;
117 use crate::ui::prompt::Completion;
118+ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
119+ use fuzzy_matcher::FuzzyMatcher;
120+ use helix_view::theme;
121+ use std::cmp::Reverse;
118− use std::borrow::Cow;
122+ use std::{borrow::Cow, sync::Arc};
119
123
120 pub type Completer = fn(&str) -> Vec<Completion>;
124 pub type Completer = fn(&str) -> Vec<Completion>;
121
125
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+
122 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
155 // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
123 pub fn filename(input: &str) -> Vec<Completion> {
156 pub fn filename(input: &str) -> Vec<Completion> {
124 // Rust's filename handling is really annoying.
157 // Rust's filename handling is really annoying.
@@ -178,10 +211,6 @@ pub mod completers {
178
211
179 // if empty, return a list of dirs and files in current dir
212 // if empty, return a list of dirs and files in current dir
180 if let Some(file_name) = file_name {
213 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−
185 let matcher = Matcher::default();
214 let matcher = Matcher::default();
186
215
187 // inefficient, but we need to calculate the scores, filter out None, then sort.
216 // 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};
6use std::{borrow::Cow, ops::RangeFrom};
6use std::{borrow::Cow, ops::RangeFrom};
7use tui::terminal::CursorKind;
7use tui::terminal::CursorKind;
8
8
9+use helix_core::{
10+ unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
11+ unicode::width::UnicodeWidthStr,
12+};
13+
9pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
14pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
10
15
11pub struct Prompt {
16pub struct Prompt {
@@ -34,6 +39,17 @@ pub enum CompletionDirection {
34 Backward,
39 Backward,
35}
40}
36
41
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+
37impl Prompt {
53impl Prompt {
38 pub fn new(
54 pub fn new(
39 prompt: String,
55 prompt: String,
@@ -52,30 +68,120 @@ impl Prompt {
52 }
68 }
53 }
69 }
54
70
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+
55 pub fn insert_char(&mut self, c: char) {
172 pub fn insert_char(&mut self, c: char) {
56− let pos = if self.line.is_empty() {
173+ self.line.insert(self.cursor, c);
57− 0
174+ let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
58− } else {
175+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
59− self.line
60− .char_indices()
61− .nth(self.cursor)
176+ self.cursor = pos;
62− .map(|(pos, _)| pos)
63− .unwrap_or_else(|| self.line.len())
64− };
177+ }
65− self.line.insert(pos, c);
66− self.cursor += 1;
67 self.completion = (self.completion_fn)(&self.line);
178 self.completion = (self.completion_fn)(&self.line);
68 self.exit_selection();
179 self.exit_selection();
69 }
180 }
70
181
71− pub fn move_char_left(&mut self) {
182+ pub fn move_cursor(&mut self, movement: Movement) {
72− self.cursor = self.cursor.saturating_sub(1)
183+ let pos = self.eval_movement(movement);
73− }
184+ self.cursor = pos
74−
75− pub fn move_char_right(&mut self) {
76− if self.cursor < self.line.len() {
77− self.cursor += 1;
78− }
79 }
185 }
80
186
81 pub fn move_start(&mut self) {
187 pub fn move_start(&mut self) {
@@ -87,39 +193,29 @@ impl Prompt {
87 }
193 }
88
194
89 pub fn delete_char_backwards(&mut self) {
195 pub fn delete_char_backwards(&mut self) {
90− if self.cursor > 0 {
196+ let pos = self.eval_movement(Movement::BackwardChar(1));
91− let pos = self
92− .line
93− .char_indices()
94− .nth(self.cursor - 1)
197+ self.line.replace_range(pos..self.cursor, "");
95− .map(|(pos, _)| pos)
96− .expect("line is not empty");
97− self.line.remove(pos);
98− self.cursor -= 1;
198+ self.cursor = pos;
99− self.completion = (self.completion_fn)(&self.line);
199+
100− }
101 self.exit_selection();
200 self.exit_selection();
201+ self.completion = (self.completion_fn)(&self.line);
102 }
202 }
103
203
104 pub fn delete_word_backwards(&mut self) {
204 pub fn delete_word_backwards(&mut self) {
105− use helix_core::get_general_category;
205+ let pos = self.eval_movement(Movement::BackwardWord(1));
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;
206+ self.line.replace_range(pos..self.cursor, "");
113− for (nn, nc) in chars {
114− if get_general_category(nc) != cat {
115− break;
116− }
117− i = nn;
118− self.cursor -= 1;
207+ self.cursor = pos;
119− }
208+
120− self.line.drain(i..);
209+ self.exit_selection();
121 self.completion = (self.completion_fn)(&self.line);
210 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+
122 self.exit_selection();
217 self.exit_selection();
218+ self.completion = (self.completion_fn)(&self.line);
123 }
219 }
124
220
125 pub fn clear(&mut self) {
221 pub fn clear(&mut self) {
@@ -293,31 +389,71 @@ impl Component for Prompt {
293 (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
389 (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
294 }
390 }
295 KeyEvent {
391 KeyEvent {
392+ code: KeyCode::Char('c'),
393+ modifiers: KeyModifiers::CONTROL,
394+ }
395+ | KeyEvent {
296 code: KeyCode::Esc, ..
396 code: KeyCode::Esc, ..
297 } => {
397 } => {
298 (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
398 (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
299 return close_fn;
399 return close_fn;
300 }
400 }
301 KeyEvent {
401 KeyEvent {
402+ code: KeyCode::Char('f'),
403+ modifiers: KeyModifiers::CONTROL,
404+ }
405+ | KeyEvent {
302 code: KeyCode::Right,
406 code: KeyCode::Right,
303 ..
407 ..
304− } => self.move_char_right(),
408+ } => self.move_cursor(Movement::ForwardChar(1)),
305 KeyEvent {
409 KeyEvent {
410+ code: KeyCode::Char('b'),
411+ modifiers: KeyModifiers::CONTROL,
412+ }
413+ | KeyEvent {
306 code: KeyCode::Left,
414 code: KeyCode::Left,
307 ..
415 ..
308− } => self.move_char_left(),
416+ } => self.move_cursor(Movement::BackwardChar(1)),
309 KeyEvent {
417 KeyEvent {
418+ code: KeyCode::End,
419+ modifiers: KeyModifiers::NONE,
420+ }
421+ | KeyEvent {
310 code: KeyCode::Char('e'),
422 code: KeyCode::Char('e'),
311 modifiers: KeyModifiers::CONTROL,
423 modifiers: KeyModifiers::CONTROL,
312 } => self.move_end(),
424 } => self.move_end(),
313 KeyEvent {
425 KeyEvent {
426+ code: KeyCode::Home,
427+ modifiers: KeyModifiers::NONE,
428+ }
429+ | KeyEvent {
314 code: KeyCode::Char('a'),
430 code: KeyCode::Char('a'),
315 modifiers: KeyModifiers::CONTROL,
431 modifiers: KeyModifiers::CONTROL,
316 } => self.move_start(),
432 } => 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)),
317 KeyEvent {
449 KeyEvent {
318 code: KeyCode::Char('w'),
450 code: KeyCode::Char('w'),
319 modifiers: KeyModifiers::CONTROL,
451 modifiers: KeyModifiers::CONTROL,
320 } => self.delete_word_backwards(),
452 } => self.delete_word_backwards(),
453+ KeyEvent {
454+ code: KeyCode::Char('k'),
455+ modifiers: KeyModifiers::CONTROL,
456+ } => self.kill_to_end_of_line(),
321 KeyEvent {
457 KeyEvent {
322 code: KeyCode::Backspace,
458 code: KeyCode::Backspace,
323 modifiers: KeyModifiers::NONE,
459 modifiers: KeyModifiers::NONE,
@@ -363,7 +499,9 @@ impl Component for Prompt {
363 (
499 (
364 Some(Position::new(
500 Some(Position::new(
365 area.y as usize + line,
501 area.y as usize + line,
502+ area.x as usize
366− area.x as usize + self.prompt.len() + self.cursor,
503+ + self.prompt.len()
504+ + UnicodeWidthStr::width(&self.line[..self.cursor]),
367 )),
505 )),
368 CursorKind::Block,
506 CursorKind::Block,
369 )
507 )
helix-tui/src/buffer.rs+2−19
@@ -203,16 +203,6 @@ impl Buffer {
203 /// # Panics
203 /// # Panics
204 ///
204 ///
205 /// Panics when given an coordinate that is outside of this Buffer's area.
205 /// 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− /// ```
216 pub fn index_of(&self, x: u16, y: u16) -> usize {
206 pub fn index_of(&self, x: u16, y: u16) -> usize {
217 debug_assert!(
207 debug_assert!(
218 x >= self.area.left()
208 x >= self.area.left()
@@ -245,15 +235,6 @@ impl Buffer {
245 /// # Panics
235 /// # Panics
246 ///
236 ///
247 /// Panics when given an index that is outside the Buffer's content.
237 /// 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− /// ```
257 pub fn pos_of(&self, i: usize) -> (u16, u16) {
238 pub fn pos_of(&self, i: usize) -> (u16, u16) {
258 debug_assert!(
239 debug_assert!(
259 i < self.content.len(),
240 i < self.content.len(),
@@ -510,6 +491,7 @@ mod tests {
510
491
511 #[test]
492 #[test]
512 #[should_panic(expected = "outside the buffer")]
493 #[should_panic(expected = "outside the buffer")]
494+ #[cfg(debug_assertions)]
513 fn pos_of_panics_on_out_of_bounds() {
495 fn pos_of_panics_on_out_of_bounds() {
514 let rect = Rect::new(0, 0, 10, 10);
496 let rect = Rect::new(0, 0, 10, 10);
515 let buf = Buffer::empty(rect);
497 let buf = Buffer::empty(rect);
@@ -520,6 +502,7 @@ mod tests {
520
502
521 #[test]
503 #[test]
522 #[should_panic(expected = "outside the buffer")]
504 #[should_panic(expected = "outside the buffer")]
505+ #[cfg(debug_assertions)]
523 fn index_of_panics_on_out_of_bounds() {
506 fn index_of_panics_on_out_of_bounds() {
524 let rect = Rect::new(0, 0, 10, 10);
507 let rect = Rect::new(0, 0, 10, 10);
525 let buf = Buffer::empty(rect);
508 let buf = Buffer::empty(rect);
helix-tui/src/lib.rs+1−1
@@ -44,7 +44,7 @@
44//! implement your own.
44//! implement your own.
45//!
45//!
46//! Each widget follows a builder pattern API providing a default configuration along with methods
46//! 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
48//! your widget instance an area to draw to.
48//! your widget instance an area to draw to.
49//!
49//!
50//! The following example renders a block of the size of the terminal:
50//! 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`].
2//!
2//!
3//! All widgets are implemented using the builder pattern and are consumable objects. They are not
3//! All widgets are implemented using the builder pattern and are consumable objects. They are not
4//! meant to be stored but used as *commands* to draw common figures in the UI.
4//! 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"
34serde = { version = "1.0", features = ["derive"] }
34serde = { version = "1.0", features = ["derive"] }
35toml = "0.5"
35toml = "0.5"
36log = "~0.4"
36log = "~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 @@
1use anyhow::{anyhow, Context, Error};
1use anyhow::{anyhow, Context, Error};
2−use serde::de::{self, Deserialize, Deserializer};
3use std::cell::Cell;
2use std::cell::Cell;
4−use std::collections::HashMap;
5use std::fmt::Display;
3use std::fmt::Display;
6use std::future::Future;
4use std::future::Future;
7use std::path::{Component, Path, PathBuf};
5use std::path::{Component, Path, PathBuf};
@@ -12,12 +10,14 @@ use helix_core::{
12 auto_detect_line_ending,
10 auto_detect_line_ending,
13 chars::{char_is_line_ending, char_is_whitespace},
11 chars::{char_is_line_ending, char_is_whitespace},
14 history::History,
12 history::History,
15− syntax::{LanguageConfiguration, LOADER},
13+ syntax::{self, LanguageConfiguration},
16 ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
14 ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
17 DEFAULT_LINE_ENDING,
15 DEFAULT_LINE_ENDING,
18};
16};
19
17
20−use crate::{DocumentId, ViewId};
18+use crate::{DocumentId, Theme, ViewId};
19+
20+use std::collections::HashMap;
21
21
22#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
22#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
23pub enum Mode {
23pub enum Mode {
@@ -26,40 +26,6 @@ pub enum Mode {
26 Insert,
26 Insert,
27}
27}
28
28
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−
63#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
29#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
64pub enum IndentStyle {
30pub enum IndentStyle {
65 Tabs,
31 Tabs,
@@ -127,6 +93,29 @@ impl fmt::Debug for Document {
127 }
93 }
128}
94}
129
95
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+
130/// Like std::mem::replace() except it allows the replacement value to be mapped from the
119/// Like std::mem::replace() except it allows the replacement value to be mapped from the
131/// original value.
120/// original value.
132fn take_with<T, F>(mut_ref: &mut T, closure: F)
121fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -181,7 +170,7 @@ pub fn fold_home_dir(path: &Path) -> PathBuf {
181/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
170/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
182/// fail, or on Windows returns annoying device paths. This is a problem Cargo
171/// fail, or on Windows returns annoying device paths. This is a problem Cargo
183/// needs to improve on.
172/// 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>
185pub fn normalize_path(path: &Path) -> PathBuf {
174pub fn normalize_path(path: &Path) -> PathBuf {
186 let path = expand_tilde(path);
175 let path = expand_tilde(path);
187 let mut components = path.components().peekable();
176 let mut components = path.components().peekable();
@@ -253,7 +242,11 @@ impl Document {
253 }
242 }
254
243
255 // TODO: async fn?
244 // TODO: async fn?
245+ pub fn load(
246+ path: PathBuf,
247+ theme: Option<&Theme>,
248+ config_loader: Option<&syntax::Loader>,
256− pub fn load(path: PathBuf) -> Result<Self, Error> {
249+ ) -> Result<Self, Error> {
257 use std::{fs::File, io::BufReader};
250 use std::{fs::File, io::BufReader};
258
251
259 let mut doc = if !path.exists() {
252 let mut doc = if !path.exists() {
@@ -277,6 +270,10 @@ impl Document {
277 doc.detect_indent_style();
270 doc.detect_indent_style();
278 doc.set_line_ending(line_ending);
271 doc.set_line_ending(line_ending);
279
272
273+ if let Some(loader) = config_loader {
274+ doc.detect_language(theme, loader);
275+ }
276+
280 Ok(doc)
277 Ok(doc)
281 }
278 }
282
279
@@ -351,12 +348,10 @@ impl Document {
351 }
348 }
352 }
349 }
353
350
354− fn detect_language(&mut self) {
351+ pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
355− if let Some(path) = self.path() {
352+ if let Some(path) = &self.path {
356− let loader = LOADER.get().unwrap();
357− let language_config = loader.language_config_for_file_name(path);
353+ let language_config = config_loader.language_config_for_file_name(path);
358− let scopes = loader.scopes();
359− self.set_language(language_config, scopes);
354+ self.set_language(theme, language_config);
360 }
355 }
361 }
356 }
362
357
@@ -493,18 +488,16 @@ impl Document {
493 // and error out when document is saved
488 // and error out when document is saved
494 self.path = Some(path);
489 self.path = Some(path);
495
490
496− // try detecting the language based on filepath
497− self.detect_language();
498−
499 Ok(())
491 Ok(())
500 }
492 }
501
493
502 pub fn set_language(
494 pub fn set_language(
503 &mut self,
495 &mut self,
496+ theme: Option<&Theme>,
504 language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
497 language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
505− scopes: &[String],
506 ) {
498 ) {
507 if let Some(language_config) = language_config {
499 if let Some(language_config) = language_config {
500+ let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
508 if let Some(highlight_config) = language_config.highlight_config(scopes) {
501 if let Some(highlight_config) = language_config.highlight_config(scopes) {
509 let syntax = Syntax::new(&self.text, highlight_config);
502 let syntax = Syntax::new(&self.text, highlight_config);
510 self.syntax = Some(syntax);
503 self.syntax = Some(syntax);
@@ -518,12 +511,15 @@ impl Document {
518 };
511 };
519 }
512 }
520
513
521− pub fn set_language2(&mut self, scope: &str) {
514+ pub fn set_language2(
522− let loader = LOADER.get().unwrap();
515+ &mut self,
516+ scope: &str,
517+ theme: Option<&Theme>,
518+ config_loader: Arc<syntax::Loader>,
519+ ) {
523− let language_config = loader.language_config_for_scope(scope);
520+ let language_config = config_loader.language_config_for_scope(scope);
524− let scopes = loader.scopes();
525
521
526− self.set_language(language_config, scopes);
522+ self.set_language(theme, language_config);
527 }
523 }
528
524
529 pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
525 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::clipboard::{get_clipboard_provider, ClipboardProvider};
2+use crate::{
3+ theme::{self, Theme},
4+ tree::Tree,
1−use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
5+ Document, DocumentId, RegisterSelection, View, ViewId,
6+};
7+use helix_core::syntax;
2use tui::layout::Rect;
8use tui::layout::Rect;
3use tui::terminal::CursorKind;
9use tui::terminal::CursorKind;
4
10
5use futures_util::future;
11use futures_util::future;
6−use std::path::PathBuf;
12+use std::{path::PathBuf, sync::Arc, time::Duration};
7−use std::time::Duration;
8
13
9use slotmap::SlotMap;
14use slotmap::SlotMap;
10
15
@@ -23,6 +28,10 @@ pub struct Editor {
23 pub registers: Registers,
28 pub registers: Registers,
24 pub theme: Theme,
29 pub theme: Theme,
25 pub language_servers: helix_lsp::Registry,
30 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>,
26
35
27 pub status_msg: Option<(String, Severity)>,
36 pub status_msg: Option<(String, Severity)>,
28}
37}
@@ -35,27 +44,11 @@ pub enum Action {
35}
44}
36
45
37impl Editor {
46impl Editor {
47+ pub fn new(
38− pub fn new(mut area: tui::layout::Rect) -> Self {
48+ mut area: tui::layout::Rect,
39− use helix_core::config_dir;
49+ themes: Arc<theme::Loader>,
40− let config = std::fs::read(config_dir().join("theme.toml"));
50+ config_loader: Arc<syntax::Loader>,
41− // load $HOME/.config/helix/theme.toml, fallback to default config
51+ ) -> Self {
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−
59 let language_servers = helix_lsp::Registry::new();
52 let language_servers = helix_lsp::Registry::new();
60
53
61 // HAXX: offset the render area height by 1 to account for prompt/commandline
54 // HAXX: offset the render area height by 1 to account for prompt/commandline
@@ -66,9 +59,12 @@ impl Editor {
66 documents: SlotMap::with_key(),
59 documents: SlotMap::with_key(),
67 count: None,
60 count: None,
68 selected_register: RegisterSelection::default(),
61 selected_register: RegisterSelection::default(),
69− theme,
62+ theme: themes.default(),
70 language_servers,
63 language_servers,
64+ syn_loader: config_loader,
65+ theme_loader: themes,
71 registers: Registers::default(),
66 registers: Registers::default(),
67+ clipboard_provider: get_clipboard_provider(),
72 status_msg: None,
68 status_msg: None,
73 }
69 }
74 }
70 }
@@ -85,6 +81,32 @@ impl Editor {
85 self.status_msg = Some((error, Severity::Error));
81 self.status_msg = Some((error, Severity::Error));
86 }
82 }
87
83
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+
88 fn _refresh(&mut self) {
110 fn _refresh(&mut self) {
89 for (view, _) in self.tree.views_mut() {
111 for (view, _) in self.tree.views_mut() {
90 let doc = &self.documents[view.doc];
112 let doc = &self.documents[view.doc];
@@ -168,7 +190,7 @@ impl Editor {
168 let id = if let Some(id) = id {
190 let id = if let Some(id) = id {
169 id
191 id
170 } else {
192 } else {
171− let mut doc = Document::load(path)?;
193+ let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?;
172
194
173 // try to find a language server based on the language name
195 // try to find a language server based on the language name
174 let language_server = doc
196 let language_server = doc
@@ -254,6 +276,10 @@ impl Editor {
254 self.documents.iter().map(|(_id, doc)| doc)
276 self.documents.iter().map(|(_id, doc)| doc)
255 }
277 }
256
278
279+ pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
280+ self.documents.iter_mut().map(|(_id, doc)| doc)
281+ }
282+
257 // pub fn current_document(&self) -> Document {
283 // pub fn current_document(&self) -> Document {
258 // let id = self.view().doc;
284 // let id = self.view().doc;
259 // let doc = &mut editor.documents[id];
285 // 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 @@
1#[macro_use]
1#[macro_use]
2pub mod macros;
2pub mod macros;
3
3
4+pub mod clipboard;
4pub mod document;
5pub mod document;
5pub mod editor;
6pub mod editor;
6−pub mod input;
7pub mod register_selection;
7pub mod register_selection;
8pub mod theme;
8pub mod theme;
9pub mod tree;
9pub mod tree;
10pub mod view;
10pub mod view;
11
11
12−slotmap::new_key_type! {
12+use slotmap::new_key_type;
13− pub struct DocumentId;
13+new_key_type! { pub struct DocumentId; }
14− pub struct ViewId;
14+new_key_type! { pub struct ViewId; }
15−}
16
15
17pub use document::Document;
16pub use document::Document;
18pub use editor::Editor;
17pub use editor::Editor;
helix-view/src/theme.rs+84−2
@@ -1,6 +1,11 @@
1+use std::{
1−use std::collections::HashMap;
2+ collections::HashMap,
3+ path::{Path, PathBuf},
4+};
2
5
6+use anyhow::Context;
3use log::warn;
7use log::warn;
8+use once_cell::sync::Lazy;
4use serde::{Deserialize, Deserializer};
9use serde::{Deserialize, Deserializer};
5use toml::Value;
10use toml::Value;
6
11
@@ -86,7 +91,84 @@ pub use tui::style::{Color, Modifier, Style};
86// }
91// }
87
92
88/// Color theme for syntax highlighting.
93/// Color theme for syntax highlighting.
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+
89−#[derive(Debug)]
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)]
90pub struct Theme {
172pub struct Theme {
91 scopes: Vec<String>,
173 scopes: Vec<String>,
92 styles: HashMap<String, Style>,
174 styles: HashMap<String, Style>,
helix-view/src/tree.rs+4
@@ -434,6 +434,10 @@ impl Tree {
434 self.focus = key;
434 self.focus = key;
435 }
435 }
436 }
436 }
437+
438+ pub fn area(&self) -> Rect {
439+ self.area
440+ }
437}
441}
438
442
439#[derive(Debug)]
443#[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.