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.
pull/224/head
Nathan Vegdahl 3 years ago
commit e686c3e462

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

@ -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

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

24
Cargo.lock generated

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

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

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

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

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

@ -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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save