Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection

Rebasing was making me manually fix conflicts on every commit, so
merging instead.
This commit is contained in:
Nathan Vegdahl 2021-06-20 16:09:14 -07:00
commit e686c3e462
40 changed files with 1587 additions and 769 deletions

4
.github/ISSUE_TEMPLATE/blank_issue.md vendored Normal file
View File

@ -0,0 +1,4 @@
---
name: Blank Issue
about: Create a blank issue.
---

View File

@ -0,0 +1,13 @@
---
name: Feature request
about: Suggest a new feature or improvement
title: ''
labels: C-enchancement
assignees: ''
---
<!-- Your feature may already be reported!
Please search on the issue tracker before creating one. -->
#### Describe your feature request

View File

@ -68,7 +68,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: --locked args: --release --locked
- name: Build release binary - name: Build release binary
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1

24
Cargo.lock generated
View File

@ -17,6 +17,12 @@ version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
[[package]]
name = "arc-swap"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.0.1"
@ -134,6 +140,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "etcetera" name = "etcetera"
version = "0.3.2" version = "0.3.2"
@ -254,6 +266,7 @@ dependencies = [
name = "helix-core" name = "helix-core"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"arc-swap",
"etcetera", "etcetera",
"helix-syntax", "helix-syntax",
"once_cell", "once_cell",
@ -354,6 +367,7 @@ dependencies = [
"tokio", "tokio",
"toml", "toml",
"url", "url",
"which",
] ]
[[package]] [[package]]
@ -1057,6 +1071,16 @@ version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "which"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
dependencies = [
"either",
"libc",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -3,6 +3,7 @@ # Summary
- [Installation](./install.md) - [Installation](./install.md)
- [Usage](./usage.md) - [Usage](./usage.md)
- [Configuration](./configuration.md) - [Configuration](./configuration.md)
- [Themes](./themes.md)
- [Keymap](./keymap.md) - [Keymap](./keymap.md)
- [Key Remapping](./remapping.md) - [Key Remapping](./remapping.md)
- [Hooks](./hooks.md) - [Hooks](./hooks.md)

View File

@ -1,97 +1,10 @@
# Configuration # Configuration
To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
## LSP ## LSP
To disable language server progress report from being displayed in the status bar add this option to your `config.toml`: To disable language server progress report from being displayed in the status bar add this option to your `config.toml`:
```toml ```toml
lsp-progress = false lsp-progress = false
``` ```
## Theme
Use a custom theme by placing a theme.toml in your config directory (i.e ~/.config/helix/theme.toml). The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/contrib/themes).
Styles in theme.toml are specified of in the form:
```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
```
where `name` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
To specify only the foreground color:
```toml
key = "#ffffff"
```
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
```toml
"key.key" = "#ffffff"
```
Possible modifiers:
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
Possible keys:
| Key | Notes |
| --- | --- |
| `attribute` | |
| `keyword` | |
| `keyword.directive` | Preprocessor directives (\#if in C) |
| `namespace` | |
| `punctuation` | |
| `punctuation.delimiter` | |
| `operator` | |
| `special` | |
| `property` | |
| `variable` | |
| `variable.parameter` | |
| `type` | |
| `type.builtin` | |
| `constructor` | |
| `function` | |
| `function.macro` | |
| `function.builtin` | |
| `comment` | |
| `variable.builtin` | |
| `constant` | |
| `constant.builtin` | |
| `string` | |
| `number` | |
| `escape` | Escaped characters |
| `label` | For lifetimes |
| `module` | |
| `ui.background` | |
| `ui.linenr` | |
| `ui.linenr.selected` | For lines with cursors |
| `ui.statusline` | |
| `ui.popup` | |
| `ui.window` | |
| `ui.help` | |
| `ui.text` | |
| `ui.text.focus` | |
| `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area |
| `warning` | LSP warning |
| `error` | LSP error |
| `info` | LSP info |
| `hint` | LSP hint |
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.

View File

@ -69,9 +69,8 @@ ### Selection manipulation
| `;` | Collapse selection onto a single cursor | | `;` | Collapse selection onto a single cursor |
| `Alt-;` | Flip selection cursor and anchor | | `Alt-;` | Flip selection cursor and anchor |
| `%` | Select entire file | | `%` | Select entire file |
| `x` | Select current line | | `x` | Select current line, if already selected, extend to next line |
| `X` | Extend to next line | | `` | Expand selection to parent syntax node TODO: pick a key |
| `[` | Expand selection to parent syntax node TODO: pick a key |
| `J` | join lines inside selection | | `J` | join lines inside selection |
| `K` | keep selections matching the regex TODO: overlapped by hover help | | `K` | keep selections matching the regex TODO: overlapped by hover help |
| `Space` | keep only the primary selection TODO: overlapped by space mode | | `Space` | keep only the primary selection TODO: overlapped by space mode |
@ -155,10 +154,10 @@ ## Window mode
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| `w`, `ctrl-w` | Switch to next window | | `w`, `Ctrl-w` | Switch to next window |
| `v`, `ctrl-v` | Vertical right split | | `v`, `Ctrl-v` | Vertical right split |
| `h`, `ctrl-h` | Horizontal bottom split | | `h`, `Ctrl-h` | Horizontal bottom split |
| `q`, `ctrl-q` | Close current window | | `q`, `Ctrl-q` | Close current window |
## Space mode ## Space mode
@ -171,6 +170,11 @@ ## Space mode
| `s` | Open symbol picker (current document) | | `s` | Open symbol picker (current document) |
| `w` | Enter [window mode](#window-mode) | | `w` | Enter [window mode](#window-mode) |
| `space` | Keep primary selection TODO: it's here because space mode replaced it | | `space` | Keep primary selection TODO: it's here because space mode replaced it |
| `p` | paste system clipboard after selections |
| `P` | paste system clipboard before selections |
| `y` | join and yank selections to clipboard |
| `Y` | yank main selection to clipboard |
| `R` | replace selections by clipboard contents |
# Picker # Picker
@ -184,4 +188,4 @@ # Picker
| `Enter` | Open selected | | `Enter` | Open selected |
| `Ctrl-h` | Open horizontally | | `Ctrl-h` | Open horizontally |
| `Ctrl-v` | Open vertically | | `Ctrl-v` | Open vertically |
| `Escape`, `ctrl-c` | Close picker | | `Escape`, `Ctrl-c` | Close picker |

View File

@ -22,27 +22,29 @@ # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
Control, Shift and Alt modifiers are encoded respectively with the prefixes Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows: `C-`, `S-` and `A-`. Special keys are encoded as follows:
* Backspace => "backspace" | Key name | Representation |
* Space => "space" | --- | --- |
* Return/Enter => "ret" | Backspace | `"backspace"` |
* < => "lt" | Space | `"space"` |
* \> => "gt" | Return/Enter | `"ret"` |
* \+ => "plus" | < | `"lt"` |
* \- => "minus" | \> | `"gt"` |
* ; => "semicolon" | \+ | `"plus"` |
* % => "percent" | \- | `"minus"` |
* Left => "left" | ; | `"semicolon"` |
* Right => "right" | % | `"percent"` |
* Up => "up" | Left | `"left"` |
* Home => "home" | Right | `"right"` |
* End => "end" | Up | `"up"` |
* Page Up => "pageup" | Home | `"home"` |
* Page Down => "pagedown" | End | `"end"` |
* Tab => "tab" | Page | `"pageup"` |
* Back Tab => "backtab" | Page | `"pagedown"` |
* Delete => "del" | Tab | `"tab"` |
* Insert => "ins" | Back | `"backtab"` |
* Null => "null" | Delete | `"del"` |
* Escape => "esc" | Insert | `"ins"` |
| Null | `"null"` |
| Escape | `"esc"` |
Commands can be found in the source code at `../../helix-term/src/commands.rs` Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)

94
book/src/themes.md Normal file
View File

@ -0,0 +1,94 @@
# Themes
First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
## Creating a theme
First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
Each line in the theme file is specified as below:
```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
```
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
To specify only the foreground color:
```toml
key = "#ffffff"
```
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
```toml
"key.key" = "#ffffff"
```
Possible modifiers:
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow\_blink` |
| `rapid\_blink` |
| `reversed` |
| `hidden` |
| `crossed\_out` |
Possible keys:
| Key | Notes |
| --- | --- |
| `attribute` | |
| `keyword` | |
| `keyword.directive` | Preprocessor directives (\#if in C) |
| `namespace` | |
| `punctuation` | |
| `punctuation.delimiter` | |
| `operator` | |
| `special` | |
| `property` | |
| `variable` | |
| `variable.parameter` | |
| `type` | |
| `type.builtin` | |
| `constructor` | |
| `function` | |
| `function.macro` | |
| `function.builtin` | |
| `comment` | |
| `variable.builtin` | |
| `constant` | |
| `constant.builtin` | |
| `string` | |
| `number` | |
| `escape` | Escaped characters |
| `label` | For lifetimes |
| `module` | |
| `ui.background` | |
| `ui.linenr` | |
| `ui.statusline` | |
| `ui.popup` | |
| `ui.window` | |
| `ui.help` | |
| `ui.text` | |
| `ui.text.focus` | |
| `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area |
| `warning` | LSP warning |
| `error` | LSP error |
| `info` | LSP info |
| `hint` | LSP hint |
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.

1
contrib/themes Symbolic link
View File

@ -0,0 +1 @@
../runtime/themes

View File

@ -19,12 +19,13 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" }
ropey = "1.3" ropey = "1.3"
smallvec = "1.4" smallvec = "1.4"
tendril = "0.4.2" tendril = "0.4.2"
unicode-segmentation = "1.7.1" unicode-segmentation = "1.7"
unicode-width = "0.1" unicode-width = "0.1"
unicode-general-category = "0.4.0" unicode-general-category = "0.4"
# slab = "0.4.2" # slab = "0.4.2"
tree-sitter = "0.19" tree-sitter = "0.19"
once_cell = "1.8" once_cell = "1.8"
arc-swap = "1"
regex = "1" regex = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@ -254,26 +254,23 @@ pub fn change<I>(document: &Document, changes: I) -> Self
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader, Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
}; };
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
let loader = Loader::new( let loader = Loader::new(Configuration {
Configuration { language: vec![LanguageConfiguration {
language: vec![LanguageConfiguration { scope: "source.rust".to_string(),
scope: "source.rust".to_string(), file_types: vec!["rs".to_string()],
file_types: vec!["rs".to_string()], language_id: Lang::Rust,
language_id: Lang::Rust, highlight_config: OnceCell::new(),
highlight_config: OnceCell::new(), //
// roots: vec![],
roots: vec![], auto_format: false,
auto_format: false, language_server: None,
language_server: None, indent: Some(IndentationConfiguration {
indent: Some(IndentationConfiguration { tab_width: 4,
tab_width: 4, unit: String::from(" "),
unit: String::from(" "), }),
}), indent_query: OnceCell::new(),
indent_query: OnceCell::new(), }],
}], });
},
Vec::new(),
);
// set runtime path so we can find the queries // set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));

View File

@ -19,6 +19,12 @@
pub mod syntax; pub mod syntax;
mod transaction; mod transaction;
pub mod unicode {
pub use unicode_general_category as category;
pub use unicode_segmentation as segmentation;
pub use unicode_width as width;
}
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> = static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
once_cell::sync::Lazy::new(runtime_dir); once_cell::sync::Lazy::new(runtime_dir);
@ -51,7 +57,7 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
} }
#[cfg(not(embed_runtime))] #[cfg(not(embed_runtime))]
fn runtime_dir() -> std::path::PathBuf { pub fn runtime_dir() -> std::path::PathBuf {
if let Ok(dir) = std::env::var("HELIX_RUNTIME") { if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
return dir.into(); return dir.into();
} }
@ -98,8 +104,6 @@ pub fn cache_dir() -> std::path::PathBuf {
pub use tendril::StrTendril as Tendril; pub use tendril::StrTendril as Tendril;
pub use unicode_general_category::get_general_category;
#[doc(inline)] #[doc(inline)]
pub use {regex, tree_sitter}; pub use {regex, tree_sitter};

View File

@ -1,6 +1,8 @@
use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction}; use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
pub use helix_syntax::{get_language, get_language_name, Lang}; pub use helix_syntax::{get_language, get_language_name, Lang};
use arc_swap::ArcSwap;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
@ -143,37 +145,49 @@ fn read_query(language: &str, filename: &str) -> String {
} }
impl LanguageConfiguration { impl LanguageConfiguration {
fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm");
// always highlight syntax errors
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = "";
if highlights_query.is_empty() {
None
} else {
let language = get_language(self.language_id);
let mut config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
locals_query,
)
.unwrap(); // TODO: no unwrap
config.configure(scopes);
Some(Arc::new(config))
}
}
pub fn reconfigure(&self, scopes: &[String]) {
if let Some(Some(config)) = self.highlight_config.get() {
config.configure(scopes);
}
}
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> { pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
self.highlight_config self.highlight_config
.get_or_init(|| { .get_or_init(|| self.initialize_highlight(scopes))
let language = get_language_name(self.language_id).to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm");
// always highlight syntax errors
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = "";
if highlights_query.is_empty() {
None
} else {
let language = get_language(self.language_id);
let mut config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
locals_query,
)
.unwrap(); // TODO: no unwrap
config.configure(scopes);
Some(Arc::new(config))
}
})
.clone() .clone()
} }
pub fn is_highlight_initialized(&self) -> bool {
self.highlight_config.get().is_some()
}
pub fn indent_query(&self) -> Option<&IndentQuery> { pub fn indent_query(&self) -> Option<&IndentQuery> {
self.indent_query self.indent_query
.get_or_init(|| { .get_or_init(|| {
@ -190,22 +204,18 @@ pub fn scope(&self) -> &str {
} }
} }
pub static LOADER: OnceCell<Loader> = OnceCell::new();
#[derive(Debug)] #[derive(Debug)]
pub struct Loader { pub struct Loader {
// highlight_names ? // highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>, language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize> language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
scopes: Vec<String>,
} }
impl Loader { impl Loader {
pub fn new(config: Configuration, scopes: Vec<String>) -> Self { pub fn new(config: Configuration) -> Self {
let mut loader = Self { let mut loader = Self {
language_configs: Vec::new(), language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(), language_config_ids_by_file_type: HashMap::new(),
scopes,
}; };
for config in config.language { for config in config.language {
@ -225,10 +235,6 @@ pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
loader loader
} }
pub fn scopes(&self) -> &[String] {
&self.scopes
}
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this file name // Find all the language configurations that match this file name
// or a suffix of the file name. // or a suffix of the file name.
@ -253,6 +259,10 @@ pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfi
.find(|config| config.scope == scope) .find(|config| config.scope == scope)
.cloned() .cloned()
} }
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
}
} }
pub struct TsParser { pub struct TsParser {
@ -772,7 +782,7 @@ pub struct HighlightConfiguration {
combined_injections_query: Option<Query>, combined_injections_query: Option<Query>,
locals_pattern_index: usize, locals_pattern_index: usize,
highlights_pattern_index: usize, highlights_pattern_index: usize,
highlight_indices: Vec<Option<Highlight>>, highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>, non_local_variable_patterns: Vec<bool>,
injection_content_capture_index: Option<u32>, injection_content_capture_index: Option<u32>,
injection_language_capture_index: Option<u32>, injection_language_capture_index: Option<u32>,
@ -924,7 +934,7 @@ pub fn new(
} }
} }
let highlight_indices = vec![None; query.capture_names().len()]; let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
Ok(Self { Ok(Self {
language, language,
query, query,
@ -957,17 +967,20 @@ pub fn names(&self) -> &[String] {
/// ///
/// When highlighting, results are returned as `Highlight` values, which contain the index /// When highlighting, results are returned as `Highlight` values, which contain the index
/// of the matched highlight this list of highlight names. /// of the matched highlight this list of highlight names.
pub fn configure(&mut self, recognized_names: &[String]) { pub fn configure(&self, recognized_names: &[String]) {
let mut capture_parts = Vec::new(); let mut capture_parts = Vec::new();
self.highlight_indices.clear(); let indices: Vec<_> = self
self.highlight_indices .query
.extend(self.query.capture_names().iter().map(move |capture_name| { .capture_names()
.iter()
.map(move |capture_name| {
capture_parts.clear(); capture_parts.clear();
capture_parts.extend(capture_name.split('.')); capture_parts.extend(capture_name.split('.'));
let mut best_index = None; let mut best_index = None;
let mut best_match_len = 0; let mut best_match_len = 0;
for (i, recognized_name) in recognized_names.iter().enumerate() { for (i, recognized_name) in recognized_names.iter().enumerate() {
let recognized_name = recognized_name;
let mut len = 0; let mut len = 0;
let mut matches = true; let mut matches = true;
for part in recognized_name.split('.') { for part in recognized_name.split('.') {
@ -983,7 +996,10 @@ pub fn configure(&mut self, recognized_names: &[String]) {
} }
} }
best_index.map(Highlight) best_index.map(Highlight)
})); })
.collect();
self.highlight_indices.store(Arc::new(indices));
} }
} }
@ -1562,7 +1578,7 @@ fn next(&mut self) -> Option<Self::Item> {
} }
} }
let current_highlight = layer.config.highlight_indices[capture.index as usize]; let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
// If this node represents a local definition, then store the current // If this node represents a local definition, then store the current
// highlight value on the local scope entry representing this node. // highlight value on the local scope entry representing this node.

View File

@ -1,7 +1,8 @@
use helix_core::syntax;
use helix_lsp::{lsp, LspProgressMap}; use helix_lsp::{lsp, LspProgressMap};
use helix_view::{document::Mode, Document, Editor, Theme, View}; use helix_view::{document::Mode, theme, Document, Editor, Theme, View};
use crate::{args::Args, compositor::Compositor, config::Config, ui}; use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
use log::{error, info}; use log::{error, info};
@ -14,7 +15,7 @@
time::Duration, time::Duration,
}; };
use anyhow::Error; use anyhow::{Context, Error};
use crossterm::{ use crossterm::{
event::{Event, EventStream}, event::{Event, EventStream},
@ -36,6 +37,8 @@ pub struct Application {
compositor: Compositor, compositor: Compositor,
editor: Editor, editor: Editor,
theme_loader: Arc<theme::Loader>,
syn_loader: Arc<syntax::Loader>,
callbacks: LspCallbacks, callbacks: LspCallbacks,
lsp_progress: LspProgressMap, lsp_progress: LspProgressMap,
@ -47,9 +50,36 @@ pub fn new(mut args: Args, config: Config) -> Result<Self, Error> {
use helix_view::editor::Action; use helix_view::editor::Action;
let mut compositor = Compositor::new()?; let mut compositor = Compositor::new()?;
let size = compositor.size(); let size = compositor.size();
let mut editor = Editor::new(size);
let mut editor_view = Box::new(ui::EditorView::new(config.keys)); let conf_dir = helix_core::config_dir();
let theme_loader =
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load $HOME/.config/helix/languages.toml, fallback to default config
let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
let lang_conf = lang_conf
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
let theme = if let Some(theme) = &config.global.theme {
match theme_loader.load(theme) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed to load theme `{}` - {}", theme, e);
theme_loader.default()
}
}
} else {
theme_loader.default()
};
let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone());
let mut editor_view = Box::new(ui::EditorView::new(config.keymaps));
compositor.push(editor_view); compositor.push(editor_view);
if !args.files.is_empty() { if !args.files.is_empty() {
@ -72,10 +102,14 @@ pub fn new(mut args: Args, config: Config) -> Result<Self, Error> {
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
} }
editor.set_theme(theme);
let mut app = Self { let mut app = Self {
compositor, compositor,
editor, editor,
theme_loader,
syn_loader,
callbacks: FuturesUnordered::new(), callbacks: FuturesUnordered::new(),
lsp_progress: LspProgressMap::new(), lsp_progress: LspProgressMap::new(),
lsp_progress_enabled: config.global.lsp_progress, lsp_progress_enabled: config.global.lsp_progress,

View File

@ -11,7 +11,6 @@
use helix_view::{ use helix_view::{
document::{IndentStyle, Mode}, document::{IndentStyle, Mode},
input::{KeyCode, KeyEvent},
view::{View, PADDING}, view::{View, PADDING},
Document, DocumentId, Editor, ViewId, Document, DocumentId, Editor, ViewId,
}; };
@ -39,8 +38,8 @@
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crossterm::event::{KeyCode, KeyEvent};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer};
pub struct Context<'a> { pub struct Context<'a> {
pub selected_register: helix_view::RegisterSelection, pub selected_register: helix_view::RegisterSelection,
@ -186,7 +185,6 @@ pub fn name(&self) -> &'static str {
search_next, search_next,
extend_search_next, extend_search_next,
search_selection, search_selection,
select_line,
extend_line, extend_line,
delete_selection, delete_selection,
change_selection, change_selection,
@ -223,9 +221,14 @@ pub fn name(&self) -> &'static str {
undo, undo,
redo, redo,
yank, yank,
yank_joined_to_clipboard,
yank_main_selection_to_clipboard,
replace_with_yanked, replace_with_yanked,
replace_selections_with_clipboard,
paste_after, paste_after,
paste_before, paste_before,
paste_clipboard_after,
paste_clipboard_before,
indent, indent,
unindent, unindent,
format_selections, format_selections,
@ -253,48 +256,6 @@ pub fn name(&self) -> &'static str {
); );
} }
impl fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.debug_tuple("Command").field(name).finish()
}
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.write_str(name)
}
}
impl std::str::FromStr for Command {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Command::COMMAND_LIST
.iter()
.copied()
.find(|cmd| cmd.0 == s)
.ok_or_else(|| anyhow!("No command named '{}'", s))
}
}
impl<'de> Deserialize<'de> for Command {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
}
}
impl PartialEq for Command {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
}
}
fn move_char_left(cx: &mut Context) { fn move_char_left(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -926,21 +887,6 @@ fn search_selection(cx: &mut Context) {
// //
fn select_line(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let pos = doc.selection(view.id).primary();
let text = doc.text();
let line = text.char_to_line(pos.head);
let start = text.line_to_char(line);
let end = text
.line_to_char(std::cmp::min(doc.text().len_lines(), line + count))
.saturating_sub(1);
doc.set_selection(view.id, Selection::single(start, end));
}
fn extend_line(cx: &mut Context) { fn extend_line(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -1318,6 +1264,57 @@ fn force_quit_all(editor: &mut Editor, args: &[&str], event: PromptEvent) {
quit_all_impl(editor, args, event, true) quit_all_impl(editor, args, event, true)
} }
fn theme(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let theme = if let Some(theme) = args.first() {
theme
} else {
editor.set_error("theme name not provided".into());
return;
};
editor.set_theme_from_name(theme);
}
fn yank_main_selection_to_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
yank_main_selection_to_clipboard_impl(editor);
}
fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], _: PromptEvent) {
let separator = args.first().copied().unwrap_or("\n");
yank_joined_to_clipboard_impl(editor, separator);
}
fn paste_clipboard_after(editor: &mut Editor, _: &[&str], _: PromptEvent) {
paste_clipboard_impl(editor, Paste::After);
}
fn paste_clipboard_before(editor: &mut Editor, _: &[&str], _: PromptEvent) {
paste_clipboard_impl(editor, Paste::After);
}
fn replace_selections_with_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
let (view, doc) = current!(editor);
match editor.clipboard_provider.get_contents() {
Ok(contents) => {
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let max_to = doc.text().len_chars().saturating_sub(1);
let to = std::cmp::min(max_to, range.to() + 1);
(range.from(), to, Some(contents.as_str().into()))
});
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
}
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
}
}
fn show_clipboard_provider(editor: &mut Editor, _: &[&str], _: PromptEvent) {
editor.set_status(editor.clipboard_provider.name().into());
}
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "quit", name: "quit",
@ -1431,7 +1428,55 @@ fn force_quit_all(editor: &mut Editor, args: &[&str], event: PromptEvent) {
fun: force_quit_all, fun: force_quit_all,
completer: None, completer: None,
}, },
TypableCommand {
name: "theme",
alias: None,
doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
fun: theme,
completer: Some(completers::theme),
},
TypableCommand {
name: "clipboard-yank",
alias: None,
doc: "Yank main selection into system clipboard.",
fun: yank_main_selection_to_clipboard,
completer: None,
},
TypableCommand {
name: "clipboard-yank-join",
alias: None,
doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
fun: yank_joined_to_clipboard,
completer: None,
},
TypableCommand {
name: "clipboard-paste-after",
alias: None,
doc: "Paste system clipboard after selections.",
fun: paste_clipboard_after,
completer: None,
},
TypableCommand {
name: "clipboard-paste-before",
alias: None,
doc: "Paste system clipboard before selections.",
fun: paste_clipboard_before,
completer: None,
},
TypableCommand {
name: "clipboard-paste-replace",
alias: None,
doc: "Replace selections with content of system clipboard.",
fun: replace_selections_with_clipboard,
completer: None,
},
TypableCommand {
name: "show-clipboard-provider",
alias: None,
doc: "Show clipboard provider name in status bar.",
fun: show_clipboard_provider,
completer: None,
},
]; ];
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| { pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@ -2424,6 +2469,52 @@ fn yank(cx: &mut Context) {
cx.editor.set_status(msg) cx.editor.set_status(msg)
} }
fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) {
let (view, doc) = current!(editor);
let values: Vec<String> = doc
.selection(view.id)
.fragments(doc.text().slice(..))
.map(Cow::into_owned)
.collect();
let msg = format!(
"joined and yanked {} selection(s) to system clipboard",
values.len(),
);
let joined = values.join(separator);
if let Err(e) = editor.clipboard_provider.set_contents(joined) {
log::error!("Couldn't set system clipboard content: {:?}", e);
}
editor.set_status(msg);
}
fn yank_joined_to_clipboard(cx: &mut Context) {
yank_joined_to_clipboard_impl(&mut cx.editor, "\n");
}
fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) {
let (view, doc) = current!(editor);
let value = doc
.selection(view.id)
.primary()
.fragment(doc.text().slice(..));
if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
log::error!("Couldn't set system clipboard content: {:?}", e);
}
editor.set_status("yanked main selection to system clipboard".to_owned());
}
fn yank_main_selection_to_clipboard(cx: &mut Context) {
yank_main_selection_to_clipboard_impl(&mut cx.editor);
}
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
enum Paste { enum Paste {
Before, Before,
@ -2469,6 +2560,31 @@ fn paste_impl(
Some(transaction) Some(transaction)
} }
fn paste_clipboard_impl(editor: &mut Editor, action: Paste) {
let (view, doc) = current!(editor);
match editor
.clipboard_provider
.get_contents()
.map(|contents| paste_impl(&[contents], doc, view, action))
{
Ok(Some(transaction)) => {
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
}
Ok(None) => {}
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
}
}
fn paste_clipboard_after(cx: &mut Context) {
paste_clipboard_impl(&mut cx.editor, Paste::After);
}
fn paste_clipboard_before(cx: &mut Context) {
paste_clipboard_impl(&mut cx.editor, Paste::Before);
}
fn replace_with_yanked(cx: &mut Context) { fn replace_with_yanked(cx: &mut Context) {
let reg_name = cx.selected_register.name(); let reg_name = cx.selected_register.name();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -2489,6 +2605,29 @@ fn replace_with_yanked(cx: &mut Context) {
} }
} }
fn replace_selections_with_clipboard_impl(editor: &mut Editor) {
let (view, doc) = current!(editor);
match editor.clipboard_provider.get_contents() {
Ok(contents) => {
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let max_to = doc.text().len_chars().saturating_sub(1);
let to = std::cmp::min(max_to, range.to() + 1);
(range.from(), to, Some(contents.as_str().into()))
});
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
}
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
}
}
fn replace_selections_with_clipboard(cx: &mut Context) {
replace_selections_with_clipboard_impl(&mut cx.editor);
}
// alt-p => paste every yanked selection after selected text // alt-p => paste every yanked selection after selected text
// alt-P => paste every yanked selection before selected text // alt-P => paste every yanked selection before selected text
// R => replace selected text with yanked text // R => replace selected text with yanked text
@ -2854,7 +2993,7 @@ fn hover(cx: &mut Context) {
// skip if contents empty // skip if contents empty
let contents = ui::Markdown::new(contents); let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let mut popup = Popup::new(contents); let mut popup = Popup::new(contents);
compositor.push(Box::new(popup)); compositor.push(Box::new(popup));
} }
@ -3009,6 +3148,11 @@ fn space_mode(cx: &mut Context) {
'b' => buffer_picker(cx), 'b' => buffer_picker(cx),
's' => symbol_picker(cx), 's' => symbol_picker(cx),
'w' => window_mode(cx), 'w' => window_mode(cx),
'y' => yank_joined_to_clipboard(cx),
'Y' => yank_main_selection_to_clipboard(cx),
'p' => paste_clipboard_after(cx),
'P' => paste_clipboard_before(cx),
'R' => replace_selections_with_clipboard(cx),
// ' ' => toggle_alternate_buffer(cx), // ' ' => toggle_alternate_buffer(cx),
// TODO: temporary since space mode took its old key // TODO: temporary since space mode took its old key
' ' => keep_primary_selection(cx), ' ' => keep_primary_selection(cx),
@ -3092,3 +3236,29 @@ fn right_bracket_mode(cx: &mut Context) {
} }
}) })
} }
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.write_str(name)
}
}
impl std::str::FromStr for Command {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Command::COMMAND_LIST
.iter()
.copied()
.find(|cmd| cmd.0 == s)
.ok_or_else(|| anyhow!("No command named '{}'", s))
}
}
impl fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.debug_tuple("Command").field(name).finish()
}
}

View File

@ -178,13 +178,13 @@ pub trait AnyComponent {
/// Returns a boxed any from a boxed self. /// Returns a boxed any from a boxed self.
/// ///
/// Can be used before `Box::downcast()`. /// Can be used before `Box::downcast()`.
/// //
/// # Examples // # Examples
/// //
/// ```rust // ```rust
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text")); // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap(); // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
/// ``` // ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>; fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
} }

View File

@ -1,63 +1,55 @@
use serde::Deserialize; use anyhow::{Error, Result};
use std::{collections::HashMap, str::FromStr};
use crate::commands::Command; use serde::{de::Error as SerdeError, Deserialize, Serialize};
use crate::keymap::Keymaps;
use crate::keymap::{parse_keymaps, Keymaps};
#[derive(Debug, PartialEq, Deserialize)]
pub struct GlobalConfig { pub struct GlobalConfig {
pub theme: Option<String>,
pub lsp_progress: bool, pub lsp_progress: bool,
} }
impl Default for GlobalConfig { impl Default for GlobalConfig {
fn default() -> Self { fn default() -> Self {
Self { lsp_progress: true } Self {
lsp_progress: true,
theme: None,
}
} }
} }
#[derive(Debug, Default, PartialEq, Deserialize)] #[derive(Default)]
#[serde(default)]
pub struct Config { pub struct Config {
pub global: GlobalConfig, pub global: GlobalConfig,
pub keys: Keymaps, pub keymaps: Keymaps,
} }
#[test] #[derive(Serialize, Deserialize)]
fn parsing_keymaps_config_file() { #[serde(rename_all = "kebab-case")]
use helix_core::hashmap; struct TomlConfig {
use helix_view::document::Mode; theme: Option<String>,
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; lsp_progress: Option<bool>,
keys: Option<HashMap<String, HashMap<String, String>>>,
let sample_keymaps = r#" }
[keys.insert]
y = "move_line_down" impl<'de> Deserialize<'de> for Config {
S-C-a = "delete_selection" fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
[keys.normal] D: serde::Deserializer<'de>,
A-F12 = "move_next_word_end" {
"#; let config = TomlConfig::deserialize(deserializer)?;
Ok(Self {
assert_eq!( global: GlobalConfig {
toml::from_str::<Config>(sample_keymaps).unwrap(), lsp_progress: config.lsp_progress.unwrap_or(true),
Config { theme: config.theme,
global: Default::default(), },
keys: Keymaps(hashmap! { keymaps: config
Mode::Insert => hashmap! { .keys
KeyEvent { .map(|r| parse_keymaps(&r))
code: KeyCode::Char('y'), .transpose()
modifiers: KeyModifiers::NONE, .map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))?
} => Command::move_line_down, .unwrap_or_else(Keymaps::default),
KeyEvent { })
code: KeyCode::Char('a'), }
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
} => Command::delete_selection,
},
Mode::Normal => hashmap! {
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::ALT,
} => Command::move_next_word_end,
},
})
}
);
} }

View File

@ -3,8 +3,6 @@
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
use serde::Deserialize;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::Display, fmt::Display,
@ -101,6 +99,14 @@
// D] = last diagnostic // D] = last diagnostic
// } // }
// #[cfg(feature = "term")]
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Clone, Debug)]
pub struct Keymap(pub HashMap<KeyEvent, Command>);
#[derive(Clone, Debug)]
pub struct Keymaps(pub HashMap<Mode, Keymap>);
#[macro_export] #[macro_export]
macro_rules! key { macro_rules! key {
($key:ident) => { ($key:ident) => {
@ -135,21 +141,9 @@ macro_rules! alt {
}; };
} }
#[derive(Debug, PartialEq, Deserialize)]
#[serde(transparent)]
pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
impl Deref for Keymaps {
type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Default for Keymaps { impl Default for Keymaps {
fn default() -> Keymaps { fn default() -> Self {
let normal = hashmap!( let normal = Keymap(hashmap!(
key!('h') => Command::move_char_left, key!('h') => Command::move_char_left,
key!('j') => Command::move_line_down, key!('j') => Command::move_line_down,
key!('k') => Command::move_line_up, key!('k') => Command::move_line_up,
@ -202,9 +196,7 @@ fn default() -> Keymaps {
key!(';') => Command::collapse_selection, key!(';') => Command::collapse_selection,
alt!(';') => Command::flip_selections, alt!(';') => Command::flip_selections,
key!('%') => Command::select_all, key!('%') => Command::select_all,
key!('x') => Command::select_line, key!('x') => Command::extend_line,
key!('X') => Command::extend_line,
// or select mode X?
// extend_to_whole_line, crop_to_whole_line // extend_to_whole_line, crop_to_whole_line
@ -283,12 +275,12 @@ fn default() -> Keymaps {
key!('z') => Command::view_mode, key!('z') => Command::view_mode,
key!('"') => Command::select_register, key!('"') => Command::select_register,
); ));
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
// because some selection operations can now be done from normal mode, some from select mode. // because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone(); let mut select = normal.clone();
select.extend( select.0.extend(
hashmap!( hashmap!(
key!('h') => Command::extend_char_left, key!('h') => Command::extend_char_left,
key!('j') => Command::extend_line_down, key!('j') => Command::extend_line_down,
@ -321,7 +313,7 @@ fn default() -> Keymaps {
// TODO: select could be normal mode with some bindings merged over // TODO: select could be normal mode with some bindings merged over
Mode::Normal => normal, Mode::Normal => normal,
Mode::Select => select, Mode::Select => select,
Mode::Insert => hashmap!( Mode::Insert => Keymap(hashmap!(
key!(Esc) => Command::normal_mode as Command, key!(Esc) => Command::normal_mode as Command,
key!(Backspace) => Command::delete_char_backward, key!(Backspace) => Command::delete_char_backward,
key!(Delete) => Command::delete_char_forward, key!(Delete) => Command::delete_char_forward,
@ -333,9 +325,313 @@ fn default() -> Keymaps {
key!(Right) => Command::move_char_right, key!(Right) => Command::move_char_right,
key!(PageUp) => Command::page_up, key!(PageUp) => Command::page_up,
key!(PageDown) => Command::page_down, key!(PageDown) => Command::page_down,
key!(Home) => Command::move_line_start,
key!(End) => Command::move_line_end,
ctrl!('x') => Command::completion, ctrl!('x') => Command::completion,
ctrl!('w') => Command::delete_word_backward, ctrl!('w') => Command::delete_word_backward,
), )),
)) ))
} }
} }
// Newtype wrapper over keys to allow toml serialization/parsing
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)]
pub struct RepresentableKeyEvent(pub KeyEvent);
impl Display for RepresentableKeyEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self(key) = self;
f.write_fmt(format_args!(
"{}{}{}",
if key.modifiers.contains(KeyModifiers::SHIFT) {
"S-"
} else {
""
},
if key.modifiers.contains(KeyModifiers::ALT) {
"A-"
} else {
""
},
if key.modifiers.contains(KeyModifiers::CONTROL) {
"C-"
} else {
""
},
))?;
match key.code {
KeyCode::Backspace => f.write_str("backspace")?,
KeyCode::Enter => f.write_str("ret")?,
KeyCode::Left => f.write_str("left")?,
KeyCode::Right => f.write_str("right")?,
KeyCode::Up => f.write_str("up")?,
KeyCode::Down => f.write_str("down")?,
KeyCode::Home => f.write_str("home")?,
KeyCode::End => f.write_str("end")?,
KeyCode::PageUp => f.write_str("pageup")?,
KeyCode::PageDown => f.write_str("pagedown")?,
KeyCode::Tab => f.write_str("tab")?,
KeyCode::BackTab => f.write_str("backtab")?,
KeyCode::Delete => f.write_str("del")?,
KeyCode::Insert => f.write_str("ins")?,
KeyCode::Null => f.write_str("null")?,
KeyCode::Esc => f.write_str("esc")?,
KeyCode::Char('<') => f.write_str("lt")?,
KeyCode::Char('>') => f.write_str("gt")?,
KeyCode::Char('+') => f.write_str("plus")?,
KeyCode::Char('-') => f.write_str("minus")?,
KeyCode::Char(';') => f.write_str("semicolon")?,
KeyCode::Char('%') => f.write_str("percent")?,
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
};
Ok(())
}
}
impl FromStr for RepresentableKeyEvent {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens: Vec<_> = s.split('-').collect();
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
"backspace" => KeyCode::Backspace,
"space" => KeyCode::Char(' '),
"ret" => KeyCode::Enter,
"lt" => KeyCode::Char('<'),
"gt" => KeyCode::Char('>'),
"plus" => KeyCode::Char('+'),
"minus" => KeyCode::Char('-'),
"semicolon" => KeyCode::Char(';'),
"percent" => KeyCode::Char('%'),
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"del" => KeyCode::Delete,
"ins" => KeyCode::Insert,
"null" => KeyCode::Null,
"esc" => KeyCode::Esc,
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
function if function.len() > 1 && function.starts_with('F') => {
let function: String = function.chars().skip(1).collect();
let function = str::parse::<u8>(&function)?;
(function > 0 && function < 13)
.then(|| KeyCode::F(function))
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
}
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
};
let mut modifiers = KeyModifiers::empty();
for token in tokens {
let flag = match token {
"S" => KeyModifiers::SHIFT,
"A" => KeyModifiers::ALT,
"C" => KeyModifiers::CONTROL,
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
};
if modifiers.contains(flag) {
return Err(anyhow!("Repeated key modifier '{}-'", token));
}
modifiers.insert(flag);
}
Ok(RepresentableKeyEvent(KeyEvent { code, modifiers }))
}
}
pub fn parse_keymaps(toml_keymaps: &HashMap<String, HashMap<String, String>>) -> Result<Keymaps> {
let mut keymaps = Keymaps::default();
for (mode, map) in toml_keymaps {
let mode = Mode::from_str(&mode)?;
for (key, command) in map {
let key = str::parse::<RepresentableKeyEvent>(&key)?;
let command = str::parse::<Command>(&command)?;
keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command);
}
}
Ok(keymaps)
}
impl Deref for Keymap {
type Target = HashMap<KeyEvent, Command>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Deref for Keymaps {
type Target = HashMap<Mode, Keymap>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Keymap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl DerefMut for Keymaps {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)]
mod test {
use crate::config::Config;
use super::*;
impl PartialEq for Command {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
}
}
#[test]
fn parsing_keymaps_config_file() {
let sample_keymaps = r#"
[keys.insert]
y = "move_line_down"
S-C-a = "delete_selection"
[keys.normal]
A-F12 = "move_next_word_end"
"#;
let config: Config = toml::from_str(sample_keymaps).unwrap();
assert_eq!(
*config
.keymaps
.0
.get(&Mode::Insert)
.unwrap()
.0
.get(&KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::NONE
})
.unwrap(),
Command::move_line_down
);
assert_eq!(
*config
.keymaps
.0
.get(&Mode::Insert)
.unwrap()
.0
.get(&KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
})
.unwrap(),
Command::delete_selection
);
assert_eq!(
*config
.keymaps
.0
.get(&Mode::Normal)
.unwrap()
.0
.get(&KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::ALT
})
.unwrap(),
Command::move_next_word_end
);
}
#[test]
fn parsing_unmodified_keys() {
assert_eq!(
str::parse::<RepresentableKeyEvent>("backspace").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("left").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>(",").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Char(','),
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("w").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("F12").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::NONE
})
);
}
fn parsing_modified_keys() {
assert_eq!(
str::parse::<RepresentableKeyEvent>("S-minus").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Char('-'),
modifiers: KeyModifiers::SHIFT
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("C-A-S-F12").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("S-C-2").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::F(2),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
})
);
}
#[test]
fn parsing_nonsensical_keys_fails() {
assert!(str::parse::<RepresentableKeyEvent>("F13").is_err());
assert!(str::parse::<RepresentableKeyEvent>("F0").is_err());
assert!(str::parse::<RepresentableKeyEvent>("aaa").is_err());
assert!(str::parse::<RepresentableKeyEvent>("S-S-a").is_err());
assert!(str::parse::<RepresentableKeyEvent>("C-A-S-C-1").is_err());
assert!(str::parse::<RepresentableKeyEvent>("FU").is_err());
assert!(str::parse::<RepresentableKeyEvent>("123").is_err());
assert!(str::parse::<RepresentableKeyEvent>("S--").is_err());
}
}

View File

@ -1,9 +1,10 @@
use anyhow::{Context, Error, Result};
use helix_term::application::Application; use helix_term::application::Application;
use helix_term::args::Args; use helix_term::args::Args;
use helix_term::config::Config; use helix_term::config::Config;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{Context, Result};
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new(); let mut base_config = fern::Dispatch::new();
@ -88,11 +89,12 @@ async fn main() -> Result<()> {
std::fs::create_dir_all(&conf_dir).ok(); std::fs::create_dir_all(&conf_dir).ok();
} }
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) { let config = std::fs::read_to_string(conf_dir.join("config.toml"))
Ok(config) => toml::from_str(&config)?, .ok()
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), .map(|s| toml::from_str(&s))
Err(err) => return Err(Error::new(err)), .transpose()?
}; .or_else(|| Some(Config::default()))
.unwrap();
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;

View File

@ -238,6 +238,9 @@ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
.language() .language()
.and_then(|scope| scope.strip_prefix("source.")) .and_then(|scope| scope.strip_prefix("source."))
.unwrap_or(""); .unwrap_or("");
let cursor_pos = doc.selection(view.id).cursor();
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.first_line) as u16;
let doc = match &option.documentation { let doc = match &option.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
@ -246,42 +249,60 @@ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
value: contents, value: contents,
})) => { })) => {
// TODO: convert to wrapped text // TODO: convert to wrapped text
Markdown::new(format!( Markdown::new(
"```{}\n{}\n```\n{}", format!(
language, "```{}\n{}\n```\n{}",
option.detail.as_deref().unwrap_or_default(), language,
contents.clone() option.detail.as_deref().unwrap_or_default(),
)) contents.clone()
),
cx.editor.syn_loader.clone(),
)
} }
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown, kind: lsp::MarkupKind::Markdown,
value: contents, value: contents,
})) => { })) => {
// TODO: set language based on doc scope // TODO: set language based on doc scope
Markdown::new(format!( Markdown::new(
"```{}\n{}\n```\n{}", format!(
language, "```{}\n{}\n```\n{}",
option.detail.as_deref().unwrap_or_default(), language,
contents.clone() option.detail.as_deref().unwrap_or_default(),
)) contents.clone()
),
cx.editor.syn_loader.clone(),
)
} }
None if option.detail.is_some() => { None if option.detail.is_some() => {
// TODO: copied from above // TODO: copied from above
// TODO: set language based on doc scope // TODO: set language based on doc scope
Markdown::new(format!( Markdown::new(
"```{}\n{}\n```", format!(
language, "```{}\n{}\n```",
option.detail.as_deref().unwrap_or_default(), language,
)) option.detail.as_deref().unwrap_or_default(),
),
cx.editor.syn_loader.clone(),
)
} }
None => return, None => return,
}; };
let half = area.height / 2; let half = area.height / 2;
let height = 15.min(half); let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits. // we want to make sure the cursor is visible (not hidden behind the documentation)
let area = Rect::new(0, area.height - height - 2, area.width, height); let y = if cursor_pos + view.area.y
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
{
0
} else {
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
area.height.saturating_sub(height).saturating_sub(2)
};
let area = Rect::new(0, y, area.width, height);
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");

View File

@ -11,13 +11,12 @@
syntax::{self, HighlightEvent}, syntax::{self, HighlightEvent},
LineEnding, Position, Range, LineEnding, Position, Range,
}; };
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
use helix_view::{document::Mode, Document, Editor, Theme, View}; use helix_view::{document::Mode, Document, Editor, Theme, View};
use std::borrow::Cow; use std::borrow::Cow;
use crossterm::{ use crossterm::{
cursor, cursor,
event::{read, Event, EventStream}, event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
}; };
use tui::{ use tui::{
backend::CrosstermBackend, backend::CrosstermBackend,
@ -130,7 +129,7 @@ pub fn render_buffer(
})], })],
}; };
let mut spans = Vec::new(); let mut spans = Vec::new();
let mut visual_x = 0; let mut visual_x = 0u16;
let mut line = 0u16; let mut line = 0u16;
let tab_width = doc.tab_width(); let tab_width = doc.tab_width();
@ -186,7 +185,7 @@ pub fn render_buffer(
break 'outer; break 'outer;
} }
} else if grapheme == "\t" { } else if grapheme == "\t" {
visual_x += (tab_width as u16); visual_x = visual_x.saturating_add(tab_width as u16);
} else { } else {
let out_of_bounds = visual_x < view.first_col as u16 let out_of_bounds = visual_x < view.first_col as u16
|| visual_x >= viewport.width + view.first_col as u16; || visual_x >= viewport.width + view.first_col as u16;
@ -198,7 +197,7 @@ pub fn render_buffer(
if out_of_bounds { if out_of_bounds {
// if we're offscreen just keep going until we hit a new line // if we're offscreen just keep going until we hit a new line
visual_x += width; visual_x = visual_x.saturating_add(width);
continue; continue;
} }
@ -608,8 +607,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
cx.editor.resize(Rect::new(0, 0, width, height - 1)); cx.editor.resize(Rect::new(0, 0, width, height - 1));
EventResult::Consumed(None) EventResult::Consumed(None)
} }
Event::Key(key) => { Event::Key(mut key) => {
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key); canonicalize_key(&mut key);
// clear status // clear status
cx.editor.status_msg = None; cx.editor.status_msg = None;

View File

@ -7,25 +7,34 @@
text::Text, text::Text,
}; };
use std::borrow::Cow; use std::{borrow::Cow, sync::Arc};
use helix_core::Position; use helix_core::{syntax, Position};
use helix_view::{Editor, Theme}; use helix_view::{Editor, Theme};
pub struct Markdown { pub struct Markdown {
contents: String, contents: String,
config_loader: Arc<syntax::Loader>,
} }
// TODO: pre-render and self reference via Pin // TODO: pre-render and self reference via Pin
// better yet, just use Tendril + subtendril for references // better yet, just use Tendril + subtendril for references
impl Markdown { impl Markdown {
pub fn new(contents: String) -> Self { pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
Self { contents } Self {
contents,
config_loader,
}
} }
} }
fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> { fn parse<'a>(
contents: &'a str,
theme: Option<&Theme>,
loader: &syntax::Loader,
) -> tui::text::Text<'a> {
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use tui::text::{Span, Spans, Text}; use tui::text::{Span, Spans, Text};
@ -79,9 +88,7 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
use helix_core::Rope; use helix_core::Rope;
let rope = Rope::from(text.as_ref()); let rope = Rope::from(text.as_ref());
let syntax = syntax::LOADER let syntax = loader
.get()
.unwrap()
.language_config_for_scope(&format!("source.{}", language)) .language_config_for_scope(&format!("source.{}", language))
.and_then(|config| config.highlight_config(theme.scopes())) .and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config)); .map(|config| Syntax::new(&rope, config));
@ -101,9 +108,7 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
} }
HighlightEvent::Source { start, end } => { HighlightEvent::Source { start, end } => {
let style = match highlights.first() { let style = match highlights.first() {
Some(span) => { Some(span) => theme.get(&theme.scopes()[span.0]),
theme.get(theme.scopes()[span.0].as_str())
}
None => text_style, None => text_style,
}; };
@ -159,7 +164,6 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
} }
} }
Event::Code(text) | Event::Html(text) => { Event::Code(text) | Event::Html(text) => {
log::warn!("code {:?}", text);
let mut span = to_span(text); let mut span = to_span(text);
span.style = code_style; span.style = code_style;
spans.push(span); spans.push(span);
@ -198,7 +202,7 @@ impl Component for Markdown {
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap}; use tui::widgets::{Paragraph, Widget, Wrap};
let text = parse(&self.contents, Some(&cx.editor.theme)); let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
let par = Paragraph::new(text) let par = Paragraph::new(text)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
@ -209,7 +213,7 @@ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
} }
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let contents = parse(&self.contents, None); let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2; let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);

View File

@ -115,10 +115,43 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
pub mod completers { pub mod completers {
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use std::borrow::Cow; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::theme;
use std::cmp::Reverse;
use std::{borrow::Cow, sync::Arc};
pub type Completer = fn(&str) -> Vec<Completion>; pub type Completer = fn(&str) -> Vec<Completion>;
pub fn theme(input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
names.extend(theme::Loader::read_names(
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
let mut names: Vec<_> = names
.into_iter()
.map(|name| ((0..), Cow::from(name)))
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(range, name)| {
matcher
.fuzzy_match(&name, &input)
.map(|score| (name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
}
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs. // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
pub fn filename(input: &str) -> Vec<Completion> { pub fn filename(input: &str) -> Vec<Completion> {
// Rust's filename handling is really annoying. // Rust's filename handling is really annoying.
@ -178,10 +211,6 @@ pub fn filename(input: &str) -> Vec<Completion> {
// if empty, return a list of dirs and files in current dir // if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name { if let Some(file_name) = file_name {
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Reverse;
let matcher = Matcher::default(); let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort. // inefficient, but we need to calculate the scores, filter out None, then sort.

View File

@ -6,6 +6,11 @@
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tui::terminal::CursorKind; use tui::terminal::CursorKind;
use helix_core::{
unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
unicode::width::UnicodeWidthStr,
};
pub type Completion = (RangeFrom<usize>, Cow<'static, str>); pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
pub struct Prompt { pub struct Prompt {
@ -34,6 +39,17 @@ pub enum CompletionDirection {
Backward, Backward,
} }
#[derive(Debug, Clone, Copy)]
pub enum Movement {
BackwardChar(usize),
BackwardWord(usize),
ForwardChar(usize),
ForwardWord(usize),
StartOfLine,
EndOfLine,
None,
}
impl Prompt { impl Prompt {
pub fn new( pub fn new(
prompt: String, prompt: String,
@ -52,30 +68,120 @@ pub fn new(
} }
} }
/// Compute the cursor position after applying movement
/// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
fn eval_movement(&self, movement: Movement) -> usize {
match movement {
Movement::BackwardChar(rep) => {
let mut position = self.cursor;
for _ in 0..rep {
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
position = pos;
} else {
break;
}
}
position
}
Movement::BackwardWord(rep) => {
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
if char_indices.is_empty() {
return self.cursor;
}
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
.unwrap_or(char_indices.len() - 1);
for _ in 0..rep {
if char_position == 0 {
break;
}
let mut found = None;
for prev in (0..char_position - 1).rev() {
if char_indices[prev].1.is_whitespace() {
found = Some(prev + 1);
break;
}
}
char_position = found.unwrap_or(0);
}
char_indices[char_position].0
}
Movement::ForwardWord(rep) => {
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
if char_indices.is_empty() {
return self.cursor;
}
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
.unwrap_or_else(|| char_indices.len());
for _ in 0..rep {
// Skip any non-whitespace characters
while char_position < char_indices.len()
&& !char_indices[char_position].1.is_whitespace()
{
char_position += 1;
}
// Skip any whitespace characters
while char_position < char_indices.len()
&& char_indices[char_position].1.is_whitespace()
{
char_position += 1;
}
// We are now on the start of the next word
}
char_indices
.get(char_position)
.map(|(i, _)| *i)
.unwrap_or_else(|| self.line.len())
}
Movement::ForwardChar(rep) => {
let mut position = self.cursor;
for _ in 0..rep {
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
position = pos;
} else {
break;
}
}
position
}
Movement::StartOfLine => 0,
Movement::EndOfLine => {
let mut cursor =
GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
pos
} else {
self.cursor
}
}
Movement::None => self.cursor,
}
}
pub fn insert_char(&mut self, c: char) { pub fn insert_char(&mut self, c: char) {
let pos = if self.line.is_empty() { self.line.insert(self.cursor, c);
0 let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
} else { if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
self.line self.cursor = pos;
.char_indices() }
.nth(self.cursor)
.map(|(pos, _)| pos)
.unwrap_or_else(|| self.line.len())
};
self.line.insert(pos, c);
self.cursor += 1;
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
self.exit_selection(); self.exit_selection();
} }
pub fn move_char_left(&mut self) { pub fn move_cursor(&mut self, movement: Movement) {
self.cursor = self.cursor.saturating_sub(1) let pos = self.eval_movement(movement);
} self.cursor = pos
pub fn move_char_right(&mut self) {
if self.cursor < self.line.len() {
self.cursor += 1;
}
} }
pub fn move_start(&mut self) { pub fn move_start(&mut self) {
@ -87,39 +193,29 @@ pub fn move_end(&mut self) {
} }
pub fn delete_char_backwards(&mut self) { pub fn delete_char_backwards(&mut self) {
if self.cursor > 0 { let pos = self.eval_movement(Movement::BackwardChar(1));
let pos = self self.line.replace_range(pos..self.cursor, "");
.line self.cursor = pos;
.char_indices()
.nth(self.cursor - 1)
.map(|(pos, _)| pos)
.expect("line is not empty");
self.line.remove(pos);
self.cursor -= 1;
self.completion = (self.completion_fn)(&self.line);
}
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
} }
pub fn delete_word_backwards(&mut self) { pub fn delete_word_backwards(&mut self) {
use helix_core::get_general_category; let pos = self.eval_movement(Movement::BackwardWord(1));
let mut chars = self.line.char_indices().rev(); self.line.replace_range(pos..self.cursor, "");
// TODO add skipping whitespace logic here self.cursor = pos;
let (mut i, cat) = match chars.next() {
Some((i, c)) => (i, get_general_category(c)),
None => return,
};
self.cursor -= 1;
for (nn, nc) in chars {
if get_general_category(nc) != cat {
break;
}
i = nn;
self.cursor -= 1;
}
self.line.drain(i..);
self.completion = (self.completion_fn)(&self.line);
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn kill_to_end_of_line(&mut self) {
let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, "");
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
@ -293,31 +389,71 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} => { } => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort); (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
return close_fn; return close_fn;
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Right, code: KeyCode::Right,
.. ..
} => self.move_char_right(), } => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent { KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Left, code: KeyCode::Left,
.. ..
} => self.move_char_left(), } => self.move_cursor(Movement::BackwardChar(1)),
KeyEvent { KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('e'), code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_end(), } => self.move_end(),
KeyEvent { KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('a'), code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_start(), } => self.move_start(),
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::ALT,
}
| KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::BackwardWord(1)),
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::ALT,
}
| KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::ForwardWord(1)),
KeyEvent { KeyEvent {
code: KeyCode::Char('w'), code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(), } => self.delete_word_backwards(),
KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => self.kill_to_end_of_line(),
KeyEvent { KeyEvent {
code: KeyCode::Backspace, code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
@ -363,7 +499,9 @@ fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind)
( (
Some(Position::new( Some(Position::new(
area.y as usize + line, area.y as usize + line,
area.x as usize + self.prompt.len() + self.cursor, area.x as usize
+ self.prompt.len()
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
)), )),
CursorKind::Block, CursorKind::Block,
) )

View File

@ -203,16 +203,6 @@ pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
/// # Panics /// # Panics
/// ///
/// Panics when given an coordinate that is outside of this Buffer's area. /// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
/// // starts at (200, 100).
/// buffer.index_of(0, 0); // Panics
/// ```
pub fn index_of(&self, x: u16, y: u16) -> usize { pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!( debug_assert!(
x >= self.area.left() x >= self.area.left()
@ -245,15 +235,6 @@ pub fn index_of(&self, x: u16, y: u16) -> usize {
/// # Panics /// # Panics
/// ///
/// Panics when given an index that is outside the Buffer's content. /// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
/// buffer.pos_of(100); // Panics
/// ```
pub fn pos_of(&self, i: usize) -> (u16, u16) { pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!( debug_assert!(
i < self.content.len(), i < self.content.len(),
@ -510,6 +491,7 @@ fn it_translates_to_and_from_coordinates() {
#[test] #[test]
#[should_panic(expected = "outside the buffer")] #[should_panic(expected = "outside the buffer")]
#[cfg(debug_assertions)]
fn pos_of_panics_on_out_of_bounds() { fn pos_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10); let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect); let buf = Buffer::empty(rect);
@ -520,6 +502,7 @@ fn pos_of_panics_on_out_of_bounds() {
#[test] #[test]
#[should_panic(expected = "outside the buffer")] #[should_panic(expected = "outside the buffer")]
#[cfg(debug_assertions)]
fn index_of_panics_on_out_of_bounds() { fn index_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10); let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect); let buf = Buffer::empty(rect);

View File

@ -44,7 +44,7 @@
//! implement your own. //! implement your own.
//! //!
//! Each widget follows a builder pattern API providing a default configuration along with methods //! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take //! to customize them. The widget is then rendered using the `Frame::render_widget` which take
//! your widget instance an area to draw to. //! your widget instance an area to draw to.
//! //!
//! The following example renders a block of the size of the terminal: //! The following example renders a block of the size of the terminal:

View File

@ -1,4 +1,4 @@
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both. //! `widgets` is a collection of types that implement [`Widget`].
//! //!
//! All widgets are implemented using the builder pattern and are consumable objects. They are not //! All widgets are implemented using the builder pattern and are consumable objects. They are not
//! meant to be stored but used as *commands* to draw common figures in the UI. //! meant to be stored but used as *commands* to draw common figures in the UI.

View File

@ -34,3 +34,6 @@ slotmap = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.5" toml = "0.5"
log = "~0.4" log = "~0.4"
which = "4.1"

193
helix-view/src/clipboard.rs Normal file
View File

@ -0,0 +1,193 @@
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
use anyhow::Result;
use std::borrow::Cow;
pub trait ClipboardProvider: std::fmt::Debug {
fn name(&self) -> Cow<str>;
fn get_contents(&self) -> Result<String>;
fn set_contents(&self, contents: String) -> Result<()>;
}
macro_rules! command_provider {
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
Box::new(provider::CommandProvider {
get_cmd: provider::CommandConfig {
prg: $get_prg,
args: &[ $( $get_arg ),* ],
},
set_cmd: provider::CommandConfig {
prg: $set_prg,
args: &[ $( $set_arg ),* ],
},
})
}};
}
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
// TODO: support for user-defined provider, probably when we have plugin support by setting a
// variable?
if exists("pbcopy") && exists("pbpaste") {
command_provider! {
paste => "pbpaste";
copy => "pbcopy";
}
} else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
command_provider! {
paste => "wl-paste", "--no-newline";
copy => "wl-copy", "--foreground", "--type", "text/plain";
}
} else if env_var_is_set("DISPLAY") && exists("xclip") {
command_provider! {
paste => "xclip", "-o", "-selection", "clipboard";
copy => "xclip", "-i", "-selection", "clipboard";
}
} else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
{
// FIXME: check performance of is_exit_success
command_provider! {
paste => "xsel", "-o", "-b";
copy => "xsel", "--nodetach", "-i", "-b";
}
} else if exists("lemonade") {
command_provider! {
paste => "lemonade", "paste";
copy => "lemonade", "copy";
}
} else if exists("doitclient") {
command_provider! {
paste => "doitclient", "wclip", "-r";
copy => "doitclient", "wclip";
}
} else if exists("win32yank.exe") {
// FIXME: does it work within WSL?
command_provider! {
paste => "win32yank.exe", "-o", "--lf";
copy => "win32yank.exe", "-i", "--crlf";
}
} else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
command_provider! {
paste => "termux-clipboard-get";
copy => "termux-clipboard-set";
}
} else if env_var_is_set("TMUX") && exists("tmux") {
command_provider! {
paste => "tmux", "save-buffer", "-";
copy => "tmux", "load-buffer", "-";
}
} else {
Box::new(provider::NopProvider)
}
}
fn exists(executable_name: &str) -> bool {
which::which(executable_name).is_ok()
}
fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}
fn is_exit_success(program: &str, args: &[&str]) -> bool {
std::process::Command::new(program)
.args(args)
.output()
.ok()
.and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
.is_some()
}
mod provider {
use super::ClipboardProvider;
use anyhow::{bail, Context as _, Result};
use std::borrow::Cow;
#[derive(Debug)]
pub struct NopProvider;
impl ClipboardProvider for NopProvider {
fn name(&self) -> Cow<str> {
Cow::Borrowed("none")
}
fn get_contents(&self) -> Result<String> {
Ok(String::new())
}
fn set_contents(&self, _: String) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct CommandConfig {
pub prg: &'static str,
pub args: &'static [&'static str],
}
impl CommandConfig {
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
use std::io::Write;
use std::process::{Command, Stdio};
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
let mut child = Command::new(self.prg)
.args(self.args)
.stdin(stdin)
.stdout(stdout)
.stderr(Stdio::null())
.spawn()?;
if let Some(input) = input {
let mut stdin = child.stdin.take().context("stdin is missing")?;
stdin
.write_all(input.as_bytes())
.context("couldn't write in stdin")?;
}
// TODO: add timer?
let output = child.wait_with_output()?;
if !output.status.success() {
bail!("clipboard provider {} failed", self.prg);
}
if pipe_output {
Ok(Some(String::from_utf8(output.stdout)?))
} else {
Ok(None)
}
}
}
#[derive(Debug)]
pub struct CommandProvider {
pub get_cmd: CommandConfig,
pub set_cmd: CommandConfig,
}
impl ClipboardProvider for CommandProvider {
fn name(&self) -> Cow<str> {
if self.get_cmd.prg != self.set_cmd.prg {
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
} else {
Cow::Borrowed(self.get_cmd.prg)
}
}
fn get_contents(&self) -> Result<String> {
let output = self
.get_cmd
.execute(None, true)?
.context("output is missing")?;
Ok(output)
}
fn set_contents(&self, value: String) -> Result<()> {
self.set_cmd.execute(Some(&value), false).map(|_| ())
}
}
}

View File

@ -1,7 +1,5 @@
use anyhow::{anyhow, Context, Error}; use anyhow::{anyhow, Context, Error};
use serde::de::{self, Deserialize, Deserializer};
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::future::Future; use std::future::Future;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
@ -12,12 +10,14 @@
auto_detect_line_ending, auto_detect_line_ending,
chars::{char_is_line_ending, char_is_whitespace}, chars::{char_is_line_ending, char_is_whitespace},
history::History, history::History,
syntax::{LanguageConfiguration, LOADER}, syntax::{self, LanguageConfiguration},
ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction, ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
DEFAULT_LINE_ENDING, DEFAULT_LINE_ENDING,
}; };
use crate::{DocumentId, ViewId}; use crate::{DocumentId, Theme, ViewId};
use std::collections::HashMap;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode { pub enum Mode {
@ -26,40 +26,6 @@ pub enum Mode {
Insert, Insert,
} }
impl Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Mode::Normal => f.write_str("normal"),
Mode::Select => f.write_str("select"),
Mode::Insert => f.write_str("insert"),
}
}
}
impl FromStr for Mode {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Mode::Normal),
"select" => Ok(Mode::Select),
"insert" => Ok(Mode::Insert),
_ => Err(anyhow!("Invalid mode '{}'", s)),
}
}
}
// toml deserializer doesn't seem to recognize string as enum
impl<'de> Deserialize<'de> for Mode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum IndentStyle { pub enum IndentStyle {
Tabs, Tabs,
@ -127,6 +93,29 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
} }
} }
impl Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Mode::Normal => f.write_str("normal"),
Mode::Select => f.write_str("select"),
Mode::Insert => f.write_str("insert"),
}
}
}
impl FromStr for Mode {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Mode::Normal),
"select" => Ok(Mode::Select),
"insert" => Ok(Mode::Insert),
_ => Err(anyhow!("Invalid mode '{}'", s)),
}
}
}
/// Like std::mem::replace() except it allows the replacement value to be mapped from the /// Like std::mem::replace() except it allows the replacement value to be mapped from the
/// original value. /// original value.
fn take_with<T, F>(mut_ref: &mut T, closure: F) fn take_with<T, F>(mut_ref: &mut T, closure: F)
@ -181,7 +170,7 @@ pub fn fold_home_dir(path: &Path) -> PathBuf {
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often /// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo /// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on. /// needs to improve on.
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81 /// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
pub fn normalize_path(path: &Path) -> PathBuf { pub fn normalize_path(path: &Path) -> PathBuf {
let path = expand_tilde(path); let path = expand_tilde(path);
let mut components = path.components().peekable(); let mut components = path.components().peekable();
@ -253,7 +242,11 @@ pub fn new(text: Rope) -> Self {
} }
// TODO: async fn? // TODO: async fn?
pub fn load(path: PathBuf) -> Result<Self, Error> { pub fn load(
path: PathBuf,
theme: Option<&Theme>,
config_loader: Option<&syntax::Loader>,
) -> Result<Self, Error> {
use std::{fs::File, io::BufReader}; use std::{fs::File, io::BufReader};
let mut doc = if !path.exists() { let mut doc = if !path.exists() {
@ -277,6 +270,10 @@ pub fn load(path: PathBuf) -> Result<Self, Error> {
doc.detect_indent_style(); doc.detect_indent_style();
doc.set_line_ending(line_ending); doc.set_line_ending(line_ending);
if let Some(loader) = config_loader {
doc.detect_language(theme, loader);
}
Ok(doc) Ok(doc)
} }
@ -351,12 +348,10 @@ pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
} }
} }
fn detect_language(&mut self) { pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
if let Some(path) = self.path() { if let Some(path) = &self.path {
let loader = LOADER.get().unwrap(); let language_config = config_loader.language_config_for_file_name(path);
let language_config = loader.language_config_for_file_name(path); self.set_language(theme, language_config);
let scopes = loader.scopes();
self.set_language(language_config, scopes);
} }
} }
@ -493,18 +488,16 @@ pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
// and error out when document is saved // and error out when document is saved
self.path = Some(path); self.path = Some(path);
// try detecting the language based on filepath
self.detect_language();
Ok(()) Ok(())
} }
pub fn set_language( pub fn set_language(
&mut self, &mut self,
theme: Option<&Theme>,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>, language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
scopes: &[String],
) { ) {
if let Some(language_config) = language_config { if let Some(language_config) = language_config {
let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
if let Some(highlight_config) = language_config.highlight_config(scopes) { if let Some(highlight_config) = language_config.highlight_config(scopes) {
let syntax = Syntax::new(&self.text, highlight_config); let syntax = Syntax::new(&self.text, highlight_config);
self.syntax = Some(syntax); self.syntax = Some(syntax);
@ -518,12 +511,15 @@ pub fn set_language(
}; };
} }
pub fn set_language2(&mut self, scope: &str) { pub fn set_language2(
let loader = LOADER.get().unwrap(); &mut self,
let language_config = loader.language_config_for_scope(scope); scope: &str,
let scopes = loader.scopes(); theme: Option<&Theme>,
config_loader: Arc<syntax::Loader>,
) {
let language_config = config_loader.language_config_for_scope(scope);
self.set_language(language_config, scopes); self.set_language(theme, language_config);
} }
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) { pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {

View File

@ -1,10 +1,15 @@
use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId}; use crate::clipboard::{get_clipboard_provider, ClipboardProvider};
use crate::{
theme::{self, Theme},
tree::Tree,
Document, DocumentId, RegisterSelection, View, ViewId,
};
use helix_core::syntax;
use tui::layout::Rect; use tui::layout::Rect;
use tui::terminal::CursorKind; use tui::terminal::CursorKind;
use futures_util::future; use futures_util::future;
use std::path::PathBuf; use std::{path::PathBuf, sync::Arc, time::Duration};
use std::time::Duration;
use slotmap::SlotMap; use slotmap::SlotMap;
@ -23,6 +28,10 @@ pub struct Editor {
pub registers: Registers, pub registers: Registers,
pub theme: Theme, pub theme: Theme,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>,
pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>,
pub status_msg: Option<(String, Severity)>, pub status_msg: Option<(String, Severity)>,
} }
@ -35,27 +44,11 @@ pub enum Action {
} }
impl Editor { impl Editor {
pub fn new(mut area: tui::layout::Rect) -> Self { pub fn new(
use helix_core::config_dir; mut area: tui::layout::Rect,
let config = std::fs::read(config_dir().join("theme.toml")); themes: Arc<theme::Loader>,
// load $HOME/.config/helix/theme.toml, fallback to default config config_loader: Arc<syntax::Loader>,
let toml = config ) -> Self {
.as_deref()
.unwrap_or(include_bytes!("../../theme.toml"));
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
// initialize language registry
use helix_core::syntax::{Loader, LOADER};
// load $HOME/.config/helix/languages.toml, fallback to default config
let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
let toml = config
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
let language_servers = helix_lsp::Registry::new(); let language_servers = helix_lsp::Registry::new();
// HAXX: offset the render area height by 1 to account for prompt/commandline // HAXX: offset the render area height by 1 to account for prompt/commandline
@ -66,9 +59,12 @@ pub fn new(mut area: tui::layout::Rect) -> Self {
documents: SlotMap::with_key(), documents: SlotMap::with_key(),
count: None, count: None,
selected_register: RegisterSelection::default(), selected_register: RegisterSelection::default(),
theme, theme: themes.default(),
language_servers, language_servers,
syn_loader: config_loader,
theme_loader: themes,
registers: Registers::default(), registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None, status_msg: None,
} }
} }
@ -85,6 +81,32 @@ pub fn set_error(&mut self, error: String) {
self.status_msg = Some((error, Severity::Error)); self.status_msg = Some((error, Severity::Error));
} }
pub fn set_theme(&mut self, theme: Theme) {
let scopes = theme.scopes();
for config in self
.syn_loader
.language_configs_iter()
.filter(|cfg| cfg.is_highlight_initialized())
{
config.reconfigure(scopes);
}
self.theme = theme;
self._refresh();
}
pub fn set_theme_from_name(&mut self, theme: &str) {
let theme = match self.theme_loader.load(theme.as_ref()) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed setting theme `{}` - {}", theme, e);
return;
}
};
self.set_theme(theme);
}
fn _refresh(&mut self) { fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() { for (view, _) in self.tree.views_mut() {
let doc = &self.documents[view.doc]; let doc = &self.documents[view.doc];
@ -168,7 +190,7 @@ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Erro
let id = if let Some(id) = id { let id = if let Some(id) = id {
id id
} else { } else {
let mut doc = Document::load(path)?; let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?;
// try to find a language server based on the language name // try to find a language server based on the language name
let language_server = doc let language_server = doc
@ -254,6 +276,10 @@ pub fn documents(&self) -> impl Iterator<Item = &Document> {
self.documents.iter().map(|(_id, doc)| doc) self.documents.iter().map(|(_id, doc)| doc)
} }
pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
self.documents.iter_mut().map(|(_id, doc)| doc)
}
// pub fn current_document(&self) -> Document { // pub fn current_document(&self) -> Document {
// let id = self.view().doc; // let id = self.view().doc;
// let doc = &mut editor.documents[id]; // let doc = &mut editor.documents[id];

View File

@ -1,226 +0,0 @@
//! Input event handling, currently backed by crossterm.
use anyhow::{anyhow, Error};
use crossterm::event;
use serde::de::{self, Deserialize, Deserializer};
use std::fmt;
pub use crossterm::event::{KeyCode, KeyModifiers};
/// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display.
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
pub struct KeyEvent {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl fmt::Display for KeyEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{}{}{}",
if self.modifiers.contains(KeyModifiers::SHIFT) {
"S-"
} else {
""
},
if self.modifiers.contains(KeyModifiers::ALT) {
"A-"
} else {
""
},
if self.modifiers.contains(KeyModifiers::CONTROL) {
"C-"
} else {
""
},
))?;
match self.code {
KeyCode::Backspace => f.write_str("backspace")?,
KeyCode::Enter => f.write_str("ret")?,
KeyCode::Left => f.write_str("left")?,
KeyCode::Right => f.write_str("right")?,
KeyCode::Up => f.write_str("up")?,
KeyCode::Down => f.write_str("down")?,
KeyCode::Home => f.write_str("home")?,
KeyCode::End => f.write_str("end")?,
KeyCode::PageUp => f.write_str("pageup")?,
KeyCode::PageDown => f.write_str("pagedown")?,
KeyCode::Tab => f.write_str("tab")?,
KeyCode::BackTab => f.write_str("backtab")?,
KeyCode::Delete => f.write_str("del")?,
KeyCode::Insert => f.write_str("ins")?,
KeyCode::Null => f.write_str("null")?,
KeyCode::Esc => f.write_str("esc")?,
KeyCode::Char('<') => f.write_str("lt")?,
KeyCode::Char('>') => f.write_str("gt")?,
KeyCode::Char('+') => f.write_str("plus")?,
KeyCode::Char('-') => f.write_str("minus")?,
KeyCode::Char(';') => f.write_str("semicolon")?,
KeyCode::Char('%') => f.write_str("percent")?,
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
};
Ok(())
}
}
impl std::str::FromStr for KeyEvent {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens: Vec<_> = s.split('-').collect();
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
"backspace" => KeyCode::Backspace,
"space" => KeyCode::Char(' '),
"ret" => KeyCode::Enter,
"lt" => KeyCode::Char('<'),
"gt" => KeyCode::Char('>'),
"plus" => KeyCode::Char('+'),
"minus" => KeyCode::Char('-'),
"semicolon" => KeyCode::Char(';'),
"percent" => KeyCode::Char('%'),
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"del" => KeyCode::Delete,
"ins" => KeyCode::Insert,
"null" => KeyCode::Null,
"esc" => KeyCode::Esc,
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
function if function.len() > 1 && function.starts_with('F') => {
let function: String = function.chars().skip(1).collect();
let function = str::parse::<u8>(&function)?;
(function > 0 && function < 13)
.then(|| KeyCode::F(function))
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
}
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
};
let mut modifiers = KeyModifiers::empty();
for token in tokens {
let flag = match token {
"S" => KeyModifiers::SHIFT,
"A" => KeyModifiers::ALT,
"C" => KeyModifiers::CONTROL,
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
};
if modifiers.contains(flag) {
return Err(anyhow!("Repeated key modifier '{}-'", token));
}
modifiers.insert(flag);
}
Ok(KeyEvent { code, modifiers })
}
}
impl<'de> Deserialize<'de> for KeyEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
}
}
impl From<event::KeyEvent> for KeyEvent {
fn from(event::KeyEvent { code, modifiers }: event::KeyEvent) -> KeyEvent {
KeyEvent { code, modifiers }
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parsing_unmodified_keys() {
assert_eq!(
str::parse::<KeyEvent>("backspace").unwrap(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>("left").unwrap(),
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>(",").unwrap(),
KeyEvent {
code: KeyCode::Char(','),
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>("w").unwrap(),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>("F12").unwrap(),
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::NONE
}
);
}
#[test]
fn parsing_modified_keys() {
assert_eq!(
str::parse::<KeyEvent>("S-minus").unwrap(),
KeyEvent {
code: KeyCode::Char('-'),
modifiers: KeyModifiers::SHIFT
}
);
assert_eq!(
str::parse::<KeyEvent>("C-A-S-F12").unwrap(),
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
}
);
assert_eq!(
str::parse::<KeyEvent>("S-C-2").unwrap(),
KeyEvent {
code: KeyCode::Char('2'),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
}
);
}
#[test]
fn parsing_nonsensical_keys_fails() {
assert!(str::parse::<KeyEvent>("F13").is_err());
assert!(str::parse::<KeyEvent>("F0").is_err());
assert!(str::parse::<KeyEvent>("aaa").is_err());
assert!(str::parse::<KeyEvent>("S-S-a").is_err());
assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err());
assert!(str::parse::<KeyEvent>("FU").is_err());
assert!(str::parse::<KeyEvent>("123").is_err());
assert!(str::parse::<KeyEvent>("S--").is_err());
}
}

View File

@ -1,18 +1,17 @@
#[macro_use] #[macro_use]
pub mod macros; pub mod macros;
pub mod clipboard;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod input;
pub mod register_selection; pub mod register_selection;
pub mod theme; pub mod theme;
pub mod tree; pub mod tree;
pub mod view; pub mod view;
slotmap::new_key_type! { use slotmap::new_key_type;
pub struct DocumentId; new_key_type! { pub struct DocumentId; }
pub struct ViewId; new_key_type! { pub struct ViewId; }
}
pub use document::Document; pub use document::Document;
pub use editor::Editor; pub use editor::Editor;

View File

@ -1,6 +1,11 @@
use std::collections::HashMap; use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use anyhow::Context;
use log::warn; use log::warn;
use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use toml::Value; use toml::Value;
@ -86,7 +91,84 @@
// } // }
/// Color theme for syntax highlighting. /// Color theme for syntax highlighting.
#[derive(Debug)]
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
#[derive(Clone, Debug)]
pub struct Loader {
user_dir: PathBuf,
default_dir: PathBuf,
}
impl Loader {
/// Creates a new loader that can load themes from two directories.
pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self {
Self {
user_dir: user_dir.as_ref().join("themes"),
default_dir: default_dir.as_ref().join("themes"),
}
}
/// Loads a theme first looking in the `user_dir` then in `default_dir`
pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
if name == "default" {
return Ok(self.default());
}
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
let path = if user_path.exists() {
user_path
} else {
self.default_dir.join(filename)
};
let data = std::fs::read(&path)?;
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
}
pub fn read_names(path: &Path) -> Vec<String> {
std::fs::read_dir(path)
.map(|entries| {
entries
.filter_map(|entry| {
if let Ok(entry) = entry {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext != "toml" {
return None;
}
return Some(
entry
.file_name()
.to_string_lossy()
.trim_end_matches(".toml")
.to_owned(),
);
}
}
None
})
.collect()
})
.unwrap_or_default()
}
/// Lists all theme names available in default and user directory
pub fn names(&self) -> Vec<String> {
let mut names = Self::read_names(&self.user_dir);
names.extend(Self::read_names(&self.default_dir));
names
}
/// Returns the default theme
pub fn default(&self) -> Theme {
DEFAULT_THEME.clone()
}
}
#[derive(Clone, Debug)]
pub struct Theme { pub struct Theme {
scopes: Vec<String>, scopes: Vec<String>,
styles: HashMap<String, Style>, styles: HashMap<String, Style>,

View File

@ -434,6 +434,10 @@ pub fn focus_next(&mut self) {
self.focus = key; self.focus = key;
} }
} }
pub fn area(&self) -> Rect {
self.area
}
} }
#[derive(Debug)] #[derive(Debug)]