Add refresh-config and open-config command (#1803)
* Add refresh-config and open-config command * clippy * Use dynamic dispatch for editor config * Refactor Result::Ok to Ok * Remove unused import * cargo fmt * Modify config error handling * cargo xtask docgen * impl display for ConfigLoadError * cargo fmt * Put keymaps behind dyn access, refactor config.load() * Update command names * Update helix-term/src/application.rs Co-authored-by: Blaž Hrastnik <blaz@mxxn.io> * Switch to unbounded_channel * Remove --edit-config command * Update configuration docs * Revert "Put keymaps behind dyn access", too hard This reverts commit 06bad8cf492b9331d0a2d1e9242f3ad4e2c1cf79. * Add refresh for keys * Refactor default_keymaps, fix config default, add test * swap -> store, remove unneeded clone * cargo fmt * Rename default_keymaps to default Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
This commit is contained in:
parent
309f2c2c8e
commit
bee05dd32a
19 changed files with 797 additions and 581 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -435,6 +435,7 @@ name = "helix-term"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"arc-swap",
|
||||||
"chrono",
|
"chrono",
|
||||||
"content_inspector",
|
"content_inspector",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
@ -483,6 +484,7 @@ name = "helix-view"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"arc-swap",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"chardetng",
|
"chardetng",
|
||||||
"clipboard-win",
|
"clipboard-win",
|
||||||
|
|
|
@ -5,7 +5,7 @@ To override global configuration parameters, create a `config.toml` file located
|
||||||
* Linux and Mac: `~/.config/helix/config.toml`
|
* Linux and Mac: `~/.config/helix/config.toml`
|
||||||
* Windows: `%AppData%\helix\config.toml`
|
* Windows: `%AppData%\helix\config.toml`
|
||||||
|
|
||||||
> Note: You may use `hx --edit-config` to create and edit the `config.toml` file.
|
> Hint: You can easily open the config file by typing `:config-open` within Helix normal mode.
|
||||||
|
|
||||||
Example config:
|
Example config:
|
||||||
|
|
||||||
|
|
|
@ -55,3 +55,5 @@
|
||||||
| `:sort` | Sort ranges in selection. |
|
| `:sort` | Sort ranges in selection. |
|
||||||
| `:rsort` | Sort ranges in selection in reverse order. |
|
| `:rsort` | Sort ranges in selection in reverse order. |
|
||||||
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
|
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
|
||||||
|
| `:config-reload` | Refreshes helix's config. |
|
||||||
|
| `:config-open` | Open the helix config.toml file. |
|
||||||
|
|
|
@ -41,6 +41,7 @@ crossterm = { version = "0.23", features = ["event-stream"] }
|
||||||
signal-hook = "0.3"
|
signal-hook = "0.3"
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||||
|
arc-swap = { version = "1.5.0" }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
fern = "0.6"
|
fern = "0.6"
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
use arc_swap::{access::Map, ArcSwap};
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
config::{default_syntax_loader, user_syntax_loader},
|
config::{default_syntax_loader, user_syntax_loader},
|
||||||
pos_at_coords, syntax, Selection,
|
pos_at_coords, syntax, Selection,
|
||||||
};
|
};
|
||||||
use helix_dap::{self as dap, Payload, Request};
|
use helix_dap::{self as dap, Payload, Request};
|
||||||
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
|
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
|
||||||
use helix_view::{editor::Breakpoint, theme, Editor};
|
use helix_view::{
|
||||||
|
editor::{Breakpoint, ConfigEvent},
|
||||||
|
theme, Editor,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -13,6 +17,7 @@ use crate::{
|
||||||
compositor::Compositor,
|
compositor::Compositor,
|
||||||
config::Config,
|
config::Config,
|
||||||
job::Jobs,
|
job::Jobs,
|
||||||
|
keymap::Keymaps,
|
||||||
ui::{self, overlay::overlayed},
|
ui::{self, overlay::overlayed},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,8 +47,7 @@ pub struct Application {
|
||||||
compositor: Compositor,
|
compositor: Compositor,
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
|
|
||||||
// TODO: share an ArcSwap with Editor?
|
config: Arc<ArcSwap<Config>>,
|
||||||
config: Config,
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
theme_loader: Arc<theme::Loader>,
|
theme_loader: Arc<theme::Loader>,
|
||||||
|
@ -56,7 +60,7 @@ pub struct Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Application {
|
impl Application {
|
||||||
pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
|
pub fn new(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();
|
||||||
|
@ -98,14 +102,20 @@ impl Application {
|
||||||
});
|
});
|
||||||
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
|
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
|
||||||
|
|
||||||
|
let config = Arc::new(ArcSwap::from_pointee(config));
|
||||||
let mut editor = Editor::new(
|
let mut editor = Editor::new(
|
||||||
size,
|
size,
|
||||||
theme_loader.clone(),
|
theme_loader.clone(),
|
||||||
syn_loader.clone(),
|
syn_loader.clone(),
|
||||||
config.editor.clone(),
|
Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||||
|
&config.editor
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
|
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||||
|
&config.keys
|
||||||
|
}));
|
||||||
|
let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
|
||||||
compositor.push(editor_view);
|
compositor.push(editor_view);
|
||||||
|
|
||||||
if args.load_tutor {
|
if args.load_tutor {
|
||||||
|
@ -113,15 +123,12 @@ impl Application {
|
||||||
editor.open(path, Action::VerticalSplit)?;
|
editor.open(path, Action::VerticalSplit)?;
|
||||||
// Unset path to prevent accidentally saving to the original tutor file.
|
// Unset path to prevent accidentally saving to the original tutor file.
|
||||||
doc_mut!(editor).set_path(None)?;
|
doc_mut!(editor).set_path(None)?;
|
||||||
} else if args.edit_config {
|
|
||||||
let path = conf_dir.join("config.toml");
|
|
||||||
editor.open(path, Action::VerticalSplit)?;
|
|
||||||
} else if !args.files.is_empty() {
|
} else if !args.files.is_empty() {
|
||||||
let first = &args.files[0].0; // we know it's not empty
|
let first = &args.files[0].0; // we know it's not empty
|
||||||
if first.is_dir() {
|
if first.is_dir() {
|
||||||
std::env::set_current_dir(&first)?;
|
std::env::set_current_dir(&first)?;
|
||||||
editor.new_file(Action::VerticalSplit);
|
editor.new_file(Action::VerticalSplit);
|
||||||
let picker = ui::file_picker(".".into(), &config.editor);
|
let picker = ui::file_picker(".".into(), &config.load().editor);
|
||||||
compositor.push(Box::new(overlayed(picker)));
|
compositor.push(Box::new(overlayed(picker)));
|
||||||
} else {
|
} else {
|
||||||
let nr_of_files = args.files.len();
|
let nr_of_files = args.files.len();
|
||||||
|
@ -228,6 +235,10 @@ impl Application {
|
||||||
Some(payload) = self.editor.debugger_events.next() => {
|
Some(payload) = self.editor.debugger_events.next() => {
|
||||||
self.handle_debugger_message(payload).await;
|
self.handle_debugger_message(payload).await;
|
||||||
}
|
}
|
||||||
|
Some(config_event) = self.editor.config_events.1.recv() => {
|
||||||
|
self.handle_config_events(config_event);
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
Some(callback) = self.jobs.futures.next() => {
|
Some(callback) = self.jobs.futures.next() => {
|
||||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||||
self.render();
|
self.render();
|
||||||
|
@ -245,6 +256,55 @@ impl Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_config_events(&mut self, config_event: ConfigEvent) {
|
||||||
|
match config_event {
|
||||||
|
ConfigEvent::Refresh => self.refresh_config(),
|
||||||
|
|
||||||
|
// Since only the Application can make changes to Editor's config,
|
||||||
|
// the Editor must send up a new copy of a modified config so that
|
||||||
|
// the Application can apply it.
|
||||||
|
ConfigEvent::Update(editor_config) => {
|
||||||
|
let mut app_config = (*self.config.load().clone()).clone();
|
||||||
|
app_config.editor = editor_config;
|
||||||
|
self.config.store(Arc::new(app_config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_config(&mut self) {
|
||||||
|
let config = Config::load(helix_loader::config_file()).unwrap_or_else(|err| {
|
||||||
|
self.editor.set_error(err.to_string());
|
||||||
|
Config::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh theme
|
||||||
|
if let Some(theme) = config.theme.clone() {
|
||||||
|
let true_color = self.true_color();
|
||||||
|
self.editor.set_theme(
|
||||||
|
self.theme_loader
|
||||||
|
.load(&theme)
|
||||||
|
.map_err(|e| {
|
||||||
|
log::warn!("failed to load theme `{}` - {}", theme, e);
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.filter(|theme| (true_color || theme.is_16_color()))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
if true_color {
|
||||||
|
self.theme_loader.default()
|
||||||
|
} else {
|
||||||
|
self.theme_loader.base16_default()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.config.store(Arc::new(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn true_color(&self) -> bool {
|
||||||
|
self.config.load().editor.true_color || crate::true_color()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
// no signal handling available on windows
|
// no signal handling available on windows
|
||||||
pub async fn handle_signals(&mut self, _signal: ()) {}
|
pub async fn handle_signals(&mut self, _signal: ()) {}
|
||||||
|
@ -700,7 +760,7 @@ impl Application {
|
||||||
self.lsp_progress.update(server_id, token, work);
|
self.lsp_progress.update(server_id, token, work);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.lsp.display_messages {
|
if self.config.load().lsp.display_messages {
|
||||||
self.editor.set_status(status);
|
self.editor.set_status(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -809,7 +869,7 @@ impl Application {
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
let mut stdout = stdout();
|
let mut stdout = stdout();
|
||||||
execute!(stdout, terminal::EnterAlternateScreen)?;
|
execute!(stdout, terminal::EnterAlternateScreen)?;
|
||||||
if self.config.editor.mouse {
|
if self.config.load().editor.mouse {
|
||||||
execute!(stdout, EnableMouseCapture)?;
|
execute!(stdout, EnableMouseCapture)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -13,7 +13,6 @@ pub struct Args {
|
||||||
pub build_grammars: bool,
|
pub build_grammars: bool,
|
||||||
pub verbosity: u64,
|
pub verbosity: u64,
|
||||||
pub files: Vec<(PathBuf, Position)>,
|
pub files: Vec<(PathBuf, Position)>,
|
||||||
pub edit_config: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
|
@ -29,7 +28,6 @@ impl Args {
|
||||||
"--version" => args.display_version = true,
|
"--version" => args.display_version = true,
|
||||||
"--help" => args.display_help = true,
|
"--help" => args.display_help = true,
|
||||||
"--tutor" => args.load_tutor = true,
|
"--tutor" => args.load_tutor = true,
|
||||||
"--edit-config" => args.edit_config = true,
|
|
||||||
"--health" => {
|
"--health" => {
|
||||||
args.health = true;
|
args.health = true;
|
||||||
args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
|
args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
|
||||||
|
|
|
@ -842,6 +842,7 @@ fn align_selections(cx: &mut Context) {
|
||||||
|
|
||||||
fn goto_window(cx: &mut Context, align: Align) {
|
fn goto_window(cx: &mut Context, align: Align) {
|
||||||
let count = cx.count() - 1;
|
let count = cx.count() - 1;
|
||||||
|
let config = cx.editor.config();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
let height = view.inner_area().height as usize;
|
let height = view.inner_area().height as usize;
|
||||||
|
@ -850,7 +851,7 @@ fn goto_window(cx: &mut Context, align: Align) {
|
||||||
// - 1 so we have at least one gap in the middle.
|
// - 1 so we have at least one gap in the middle.
|
||||||
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
|
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
|
||||||
// as we type
|
// as we type
|
||||||
let scrolloff = cx.editor.config.scrolloff.min(height.saturating_sub(1) / 2);
|
let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
|
||||||
|
|
||||||
let last_line = view.last_line(doc);
|
let last_line = view.last_line(doc);
|
||||||
|
|
||||||
|
@ -1274,6 +1275,7 @@ fn switch_to_lowercase(cx: &mut Context) {
|
||||||
|
|
||||||
pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
|
pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
|
||||||
use Direction::*;
|
use Direction::*;
|
||||||
|
let config = cx.editor.config();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
let range = doc.selection(view.id).primary();
|
let range = doc.selection(view.id).primary();
|
||||||
|
@ -1292,7 +1294,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
|
||||||
|
|
||||||
let height = view.inner_area().height;
|
let height = view.inner_area().height;
|
||||||
|
|
||||||
let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2);
|
let scrolloff = config.scrolloff.min(height as usize / 2);
|
||||||
|
|
||||||
view.offset.row = match direction {
|
view.offset.row = match direction {
|
||||||
Forward => view.offset.row + offset,
|
Forward => view.offset.row + offset,
|
||||||
|
@ -1585,8 +1587,9 @@ fn rsearch(cx: &mut Context) {
|
||||||
|
|
||||||
fn searcher(cx: &mut Context, direction: Direction) {
|
fn searcher(cx: &mut Context, direction: Direction) {
|
||||||
let reg = cx.register.unwrap_or('/');
|
let reg = cx.register.unwrap_or('/');
|
||||||
let scrolloff = cx.editor.config.scrolloff;
|
let config = cx.editor.config();
|
||||||
let wrap_around = cx.editor.config.search.wrap_around;
|
let scrolloff = config.scrolloff;
|
||||||
|
let wrap_around = config.search.wrap_around;
|
||||||
|
|
||||||
let doc = doc!(cx.editor);
|
let doc = doc!(cx.editor);
|
||||||
|
|
||||||
|
@ -1629,13 +1632,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
|
fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
|
||||||
let scrolloff = cx.editor.config.scrolloff;
|
let config = cx.editor.config();
|
||||||
|
let scrolloff = config.scrolloff;
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
let registers = &cx.editor.registers;
|
let registers = &cx.editor.registers;
|
||||||
if let Some(query) = registers.read('/') {
|
if let Some(query) = registers.read('/') {
|
||||||
let query = query.last().unwrap();
|
let query = query.last().unwrap();
|
||||||
let contents = doc.text().slice(..).to_string();
|
let contents = doc.text().slice(..).to_string();
|
||||||
let search_config = &cx.editor.config.search;
|
let search_config = &config.search;
|
||||||
let case_insensitive = if search_config.smart_case {
|
let case_insensitive = if search_config.smart_case {
|
||||||
!query.chars().any(char::is_uppercase)
|
!query.chars().any(char::is_uppercase)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1695,8 +1699,9 @@ fn search_selection(cx: &mut Context) {
|
||||||
fn global_search(cx: &mut Context) {
|
fn global_search(cx: &mut Context) {
|
||||||
let (all_matches_sx, all_matches_rx) =
|
let (all_matches_sx, all_matches_rx) =
|
||||||
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
|
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
|
||||||
let smart_case = cx.editor.config.search.smart_case;
|
let config = cx.editor.config();
|
||||||
let file_picker_config = cx.editor.config.file_picker.clone();
|
let smart_case = config.search.smart_case;
|
||||||
|
let file_picker_config = config.file_picker.clone();
|
||||||
|
|
||||||
let completions = search_completions(cx, None);
|
let completions = search_completions(cx, None);
|
||||||
let prompt = ui::regex_prompt(
|
let prompt = ui::regex_prompt(
|
||||||
|
@ -2028,7 +2033,7 @@ fn append_mode(cx: &mut Context) {
|
||||||
fn file_picker(cx: &mut Context) {
|
fn file_picker(cx: &mut Context) {
|
||||||
// We don't specify language markers, root will be the root of the current git repo
|
// We don't specify language markers, root will be the root of the current git repo
|
||||||
let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
|
let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
|
||||||
let picker = ui::file_picker(root, &cx.editor.config);
|
let picker = ui::file_picker(root, &cx.editor.config());
|
||||||
cx.push_layer(Box::new(overlayed(picker)));
|
cx.push_layer(Box::new(overlayed(picker)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2105,7 +2110,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||||
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
||||||
let doc = doc_mut!(cx.editor);
|
let doc = doc_mut!(cx.editor);
|
||||||
let keymap =
|
let keymap =
|
||||||
compositor.find::<ui::EditorView>().unwrap().keymaps.map[&doc.mode].reverse_map();
|
compositor.find::<ui::EditorView>().unwrap().keymaps.map()[&doc.mode].reverse_map();
|
||||||
|
|
||||||
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
|
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
|
||||||
commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
|
commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
|
||||||
|
@ -2571,6 +2576,7 @@ pub mod insert {
|
||||||
// It trigger completion when idle timer reaches deadline
|
// It trigger completion when idle timer reaches deadline
|
||||||
// Only trigger completion if the word under cursor is longer than n characters
|
// Only trigger completion if the word under cursor is longer than n characters
|
||||||
pub fn idle_completion(cx: &mut Context) {
|
pub fn idle_completion(cx: &mut Context) {
|
||||||
|
let config = cx.editor.config();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||||
|
@ -2578,7 +2584,7 @@ pub mod insert {
|
||||||
use helix_core::chars::char_is_word;
|
use helix_core::chars::char_is_word;
|
||||||
let mut iter = text.chars_at(cursor);
|
let mut iter = text.chars_at(cursor);
|
||||||
iter.reverse();
|
iter.reverse();
|
||||||
for _ in 0..cx.editor.config.completion_trigger_len {
|
for _ in 0..config.completion_trigger_len {
|
||||||
match iter.next() {
|
match iter.next() {
|
||||||
Some(c) if char_is_word(c) => {}
|
Some(c) if char_is_word(c) => {}
|
||||||
_ => return,
|
_ => return,
|
||||||
|
@ -4154,7 +4160,7 @@ fn shell_keep_pipe(cx: &mut Context) {
|
||||||
Some('|'),
|
Some('|'),
|
||||||
ui::completers::none,
|
ui::completers::none,
|
||||||
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
|
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
|
||||||
let shell = &cx.editor.config.shell;
|
let shell = &cx.editor.config().shell;
|
||||||
if event != PromptEvent::Validate {
|
if event != PromptEvent::Validate {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4250,7 +4256,8 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
|
||||||
Some('|'),
|
Some('|'),
|
||||||
ui::completers::none,
|
ui::completers::none,
|
||||||
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
|
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
|
||||||
let shell = &cx.editor.config.shell;
|
let config = cx.editor.config();
|
||||||
|
let shell = &config.shell;
|
||||||
if event != PromptEvent::Validate {
|
if event != PromptEvent::Validate {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4295,7 +4302,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
|
||||||
|
|
||||||
// after replace cursor may be out of bounds, do this to
|
// after replace cursor may be out of bounds, do this to
|
||||||
// make sure cursor is in view and update scroll as well
|
// make sure cursor is in view and update scroll as well
|
||||||
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
|
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use helix_view::editor::Action;
|
use helix_view::editor::{Action, ConfigEvent};
|
||||||
use ui::completers::{self, Completer};
|
use ui::completers::{self, Completer};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -540,7 +540,7 @@ fn theme(
|
||||||
.theme_loader
|
.theme_loader
|
||||||
.load(theme)
|
.load(theme)
|
||||||
.with_context(|| format!("Failed setting theme {}", theme))?;
|
.with_context(|| format!("Failed setting theme {}", theme))?;
|
||||||
let true_color = cx.editor.config.true_color || crate::true_color();
|
let true_color = cx.editor.config().true_color || crate::true_color();
|
||||||
if !(true_color || theme.is_16_color()) {
|
if !(true_color || theme.is_16_color()) {
|
||||||
bail!("Unsupported theme: theme requires true color support");
|
bail!("Unsupported theme: theme requires true color support");
|
||||||
}
|
}
|
||||||
|
@ -894,7 +894,7 @@ fn setting(
|
||||||
let key_error = || anyhow::anyhow!("Unknown key `{key}`");
|
let key_error = || anyhow::anyhow!("Unknown key `{key}`");
|
||||||
let field_error = |_| anyhow::anyhow!("Could not parse field `{arg}`");
|
let field_error = |_| anyhow::anyhow!("Could not parse field `{arg}`");
|
||||||
|
|
||||||
let mut config = serde_json::to_value(&cx.editor.config).unwrap();
|
let mut config = serde_json::to_value(&cx.editor.config().clone()).unwrap();
|
||||||
let pointer = format!("/{}", key.replace('.', "/"));
|
let pointer = format!("/{}", key.replace('.', "/"));
|
||||||
let value = config.pointer_mut(&pointer).ok_or_else(key_error)?;
|
let value = config.pointer_mut(&pointer).ok_or_else(key_error)?;
|
||||||
|
|
||||||
|
@ -904,8 +904,12 @@ fn setting(
|
||||||
} else {
|
} else {
|
||||||
arg.parse().map_err(field_error)?
|
arg.parse().map_err(field_error)?
|
||||||
};
|
};
|
||||||
cx.editor.config = serde_json::from_value(config).map_err(field_error)?;
|
let config = serde_json::from_value(config).map_err(field_error)?;
|
||||||
|
|
||||||
|
cx.editor
|
||||||
|
.config_events
|
||||||
|
.0
|
||||||
|
.send(ConfigEvent::Update(config))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,6 +999,25 @@ fn tree_sitter_subtree(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_config(
|
||||||
|
cx: &mut compositor::Context,
|
||||||
|
_args: &[Cow<str>],
|
||||||
|
_event: PromptEvent,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
cx.editor
|
||||||
|
.open(helix_loader::config_file(), Action::Replace)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_config(
|
||||||
|
cx: &mut compositor::Context,
|
||||||
|
_args: &[Cow<str>],
|
||||||
|
_event: PromptEvent,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
cx.editor.config_events.0.send(ConfigEvent::Refresh)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "quit",
|
name: "quit",
|
||||||
|
@ -1381,6 +1404,20 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
fun: tree_sitter_subtree,
|
fun: tree_sitter_subtree,
|
||||||
completer: None,
|
completer: None,
|
||||||
},
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "config-reload",
|
||||||
|
aliases: &[],
|
||||||
|
doc: "Refreshes helix's config.",
|
||||||
|
fun: refresh_config,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "config-open",
|
||||||
|
aliases: &[],
|
||||||
|
doc: "Open the helix config.toml file.",
|
||||||
|
fun: open_config,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
|
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
|
||||||
|
|
|
@ -1,25 +1,71 @@
|
||||||
|
use crate::keymap::{default::default, merge_keys, Keymap};
|
||||||
|
use helix_view::document::Mode;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::io::Error as IOError;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use toml::de::Error as TomlError;
|
||||||
|
|
||||||
use crate::keymap::Keymaps;
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub theme: Option<String>,
|
pub theme: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub lsp: LspConfig,
|
pub lsp: LspConfig,
|
||||||
#[serde(default)]
|
#[serde(default = "default")]
|
||||||
pub keys: Keymaps,
|
pub keys: HashMap<Mode, Keymap>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub editor: helix_view::editor::Config,
|
pub editor: helix_view::editor::Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
theme: None,
|
||||||
|
lsp: LspConfig::default(),
|
||||||
|
keys: default(),
|
||||||
|
editor: helix_view::editor::Config::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigLoadError {
|
||||||
|
BadConfig(TomlError),
|
||||||
|
Error(IOError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ConfigLoadError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigLoadError::BadConfig(err) => err.fmt(f),
|
||||||
|
ConfigLoadError::Error(err) => err.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
pub struct LspConfig {
|
pub struct LspConfig {
|
||||||
pub display_messages: bool,
|
pub display_messages: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> {
|
||||||
|
match std::fs::read_to_string(config_path) {
|
||||||
|
Ok(config) => toml::from_str(&config)
|
||||||
|
.map(merge_keys)
|
||||||
|
.map_err(ConfigLoadError::BadConfig),
|
||||||
|
Err(err) => Err(ConfigLoadError::Error(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_default() -> Result<Config, ConfigLoadError> {
|
||||||
|
Config::load(helix_loader::config_file())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -43,7 +89,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
toml::from_str::<Config>(sample_keymaps).unwrap(),
|
toml::from_str::<Config>(sample_keymaps).unwrap(),
|
||||||
Config {
|
Config {
|
||||||
keys: Keymaps::new(hashmap! {
|
keys: hashmap! {
|
||||||
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
|
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
|
||||||
"y" => move_line_down,
|
"y" => move_line_down,
|
||||||
"S-C-a" => delete_selection,
|
"S-C-a" => delete_selection,
|
||||||
|
@ -51,9 +97,20 @@ mod tests {
|
||||||
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
|
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
|
||||||
"A-F12" => move_next_word_end,
|
"A-F12" => move_next_word_end,
|
||||||
})),
|
})),
|
||||||
}),
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keys_resolve_to_correct_defaults() {
|
||||||
|
// From serde default
|
||||||
|
let default_keys = toml::from_str::<Config>("").unwrap().keys;
|
||||||
|
assert_eq!(default_keys, default());
|
||||||
|
|
||||||
|
// From the Default trait
|
||||||
|
let default_keys = Config::default().keys;
|
||||||
|
assert_eq!(default_keys, default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +1,23 @@
|
||||||
|
pub mod default;
|
||||||
|
pub mod macros;
|
||||||
|
|
||||||
pub use crate::commands::MappableCommand;
|
pub use crate::commands::MappableCommand;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use helix_core::hashmap;
|
use arc_swap::{
|
||||||
|
access::{DynAccess, DynGuard},
|
||||||
|
ArcSwap,
|
||||||
|
};
|
||||||
use helix_view::{document::Mode, info::Info, input::KeyEvent};
|
use helix_view::{document::Mode, info::Info, input::KeyEvent};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{BTreeSet, HashMap},
|
collections::{BTreeSet, HashMap},
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[macro_export]
|
use default::default;
|
||||||
macro_rules! key {
|
use macros::key;
|
||||||
($key:ident) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::$key,
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($($ch:tt)*) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! shift {
|
|
||||||
($key:ident) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::$key,
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($($ch:tt)*) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! ctrl {
|
|
||||||
($key:ident) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::$key,
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($($ch:tt)*) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! alt {
|
|
||||||
($key:ident) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::$key,
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($($ch:tt)*) => {
|
|
||||||
::helix_view::input::KeyEvent {
|
|
||||||
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
|
||||||
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Macro for defining the root of a `Keymap` object. Example:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use helix_core::hashmap;
|
|
||||||
/// # use helix_term::keymap;
|
|
||||||
/// # use helix_term::keymap::Keymap;
|
|
||||||
/// let normal_mode = keymap!({ "Normal mode"
|
|
||||||
/// "i" => insert_mode,
|
|
||||||
/// "g" => { "Goto"
|
|
||||||
/// "g" => goto_file_start,
|
|
||||||
/// "e" => goto_file_end,
|
|
||||||
/// },
|
|
||||||
/// "j" | "down" => move_line_down,
|
|
||||||
/// });
|
|
||||||
/// let keymap = Keymap::new(normal_mode);
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! keymap {
|
|
||||||
(@trie $cmd:ident) => {
|
|
||||||
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
|
|
||||||
};
|
|
||||||
|
|
||||||
(@trie
|
|
||||||
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
|
|
||||||
) => {
|
|
||||||
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
|
|
||||||
};
|
|
||||||
|
|
||||||
(@trie [$($cmd:ident),* $(,)?]) => {
|
|
||||||
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
|
|
||||||
) => {
|
|
||||||
// modified from the hashmap! macro
|
|
||||||
{
|
|
||||||
let _cap = hashmap!(@count $($($key),+),*);
|
|
||||||
let mut _map = ::std::collections::HashMap::with_capacity(_cap);
|
|
||||||
let mut _order = ::std::vec::Vec::with_capacity(_cap);
|
|
||||||
$(
|
|
||||||
$(
|
|
||||||
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
|
|
||||||
let _duplicate = _map.insert(
|
|
||||||
_key,
|
|
||||||
keymap!(@trie $value)
|
|
||||||
);
|
|
||||||
assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
|
|
||||||
_order.push(_key);
|
|
||||||
)+
|
|
||||||
)*
|
|
||||||
let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
|
|
||||||
$( _node.is_sticky = $sticky; )?
|
|
||||||
$crate::keymap::KeyTrie::Node(_node)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct KeyTrieNode {
|
pub struct KeyTrieNode {
|
||||||
|
@ -381,23 +269,17 @@ impl Default for Keymap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
||||||
pub struct Keymaps {
|
pub struct Keymaps {
|
||||||
#[serde(flatten)]
|
pub map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>,
|
||||||
pub map: HashMap<Mode, Keymap>,
|
|
||||||
|
|
||||||
/// Stores pending keys waiting for the next key. This is relative to a
|
/// Stores pending keys waiting for the next key. This is relative to a
|
||||||
/// sticky node if one is in use.
|
/// sticky node if one is in use.
|
||||||
#[serde(skip)]
|
|
||||||
state: Vec<KeyEvent>,
|
state: Vec<KeyEvent>,
|
||||||
|
|
||||||
/// Stores the sticky node if one is activated.
|
/// Stores the sticky node if one is activated.
|
||||||
#[serde(skip)]
|
|
||||||
pub sticky: Option<KeyTrieNode>,
|
pub sticky: Option<KeyTrieNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keymaps {
|
impl Keymaps {
|
||||||
pub fn new(map: HashMap<Mode, Keymap>) -> Self {
|
pub fn new(map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map,
|
map,
|
||||||
state: Vec::new(),
|
state: Vec::new(),
|
||||||
|
@ -405,6 +287,10 @@ impl Keymaps {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map(&self) -> DynGuard<HashMap<Mode, Keymap>> {
|
||||||
|
self.map.load()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns list of keys waiting to be disambiguated in current mode.
|
/// Returns list of keys waiting to be disambiguated in current mode.
|
||||||
pub fn pending(&self) -> &[KeyEvent] {
|
pub fn pending(&self) -> &[KeyEvent] {
|
||||||
&self.state
|
&self.state
|
||||||
|
@ -419,7 +305,8 @@ impl Keymaps {
|
||||||
/// sticky node is in use, it will be cleared.
|
/// sticky node is in use, it will be cleared.
|
||||||
pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
|
pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
|
||||||
// TODO: remove the sticky part and look up manually
|
// TODO: remove the sticky part and look up manually
|
||||||
let keymap = &self.map[&mode];
|
let keymaps = &*self.map();
|
||||||
|
let keymap = &keymaps[&mode];
|
||||||
|
|
||||||
if key!(Esc) == key {
|
if key!(Esc) == key {
|
||||||
if !self.state.is_empty() {
|
if !self.state.is_empty() {
|
||||||
|
@ -470,372 +357,25 @@ impl Keymaps {
|
||||||
|
|
||||||
impl Default for Keymaps {
|
impl Default for Keymaps {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let normal = keymap!({ "Normal mode"
|
Self::new(Box::new(ArcSwap::new(Arc::new(default()))))
|
||||||
"h" | "left" => move_char_left,
|
|
||||||
"j" | "down" => move_line_down,
|
|
||||||
"k" | "up" => move_line_up,
|
|
||||||
"l" | "right" => move_char_right,
|
|
||||||
|
|
||||||
"t" => find_till_char,
|
|
||||||
"f" => find_next_char,
|
|
||||||
"T" => till_prev_char,
|
|
||||||
"F" => find_prev_char,
|
|
||||||
"r" => replace,
|
|
||||||
"R" => replace_with_yanked,
|
|
||||||
"A-." => repeat_last_motion,
|
|
||||||
|
|
||||||
"~" => switch_case,
|
|
||||||
"`" => switch_to_lowercase,
|
|
||||||
"A-`" => switch_to_uppercase,
|
|
||||||
|
|
||||||
"home" => goto_line_start,
|
|
||||||
"end" => goto_line_end,
|
|
||||||
|
|
||||||
"w" => move_next_word_start,
|
|
||||||
"b" => move_prev_word_start,
|
|
||||||
"e" => move_next_word_end,
|
|
||||||
|
|
||||||
"W" => move_next_long_word_start,
|
|
||||||
"B" => move_prev_long_word_start,
|
|
||||||
"E" => move_next_long_word_end,
|
|
||||||
|
|
||||||
"v" => select_mode,
|
|
||||||
"G" => goto_line,
|
|
||||||
"g" => { "Goto"
|
|
||||||
"g" => goto_file_start,
|
|
||||||
"e" => goto_last_line,
|
|
||||||
"f" => goto_file,
|
|
||||||
"h" => goto_line_start,
|
|
||||||
"l" => goto_line_end,
|
|
||||||
"s" => goto_first_nonwhitespace,
|
|
||||||
"d" => goto_definition,
|
|
||||||
"y" => goto_type_definition,
|
|
||||||
"r" => goto_reference,
|
|
||||||
"i" => goto_implementation,
|
|
||||||
"t" => goto_window_top,
|
|
||||||
"c" => goto_window_center,
|
|
||||||
"b" => goto_window_bottom,
|
|
||||||
"a" => goto_last_accessed_file,
|
|
||||||
"m" => goto_last_modified_file,
|
|
||||||
"n" => goto_next_buffer,
|
|
||||||
"p" => goto_previous_buffer,
|
|
||||||
"." => goto_last_modification,
|
|
||||||
},
|
|
||||||
":" => command_mode,
|
|
||||||
|
|
||||||
"i" => insert_mode,
|
|
||||||
"I" => prepend_to_line,
|
|
||||||
"a" => append_mode,
|
|
||||||
"A" => append_to_line,
|
|
||||||
"o" => open_below,
|
|
||||||
"O" => open_above,
|
|
||||||
|
|
||||||
"d" => delete_selection,
|
|
||||||
"A-d" => delete_selection_noyank,
|
|
||||||
"c" => change_selection,
|
|
||||||
"A-c" => change_selection_noyank,
|
|
||||||
|
|
||||||
"C" => copy_selection_on_next_line,
|
|
||||||
"A-C" => copy_selection_on_prev_line,
|
|
||||||
|
|
||||||
|
|
||||||
"s" => select_regex,
|
|
||||||
"A-s" => split_selection_on_newline,
|
|
||||||
"S" => split_selection,
|
|
||||||
";" => collapse_selection,
|
|
||||||
"A-;" => flip_selections,
|
|
||||||
"A-k" | "A-up" => expand_selection,
|
|
||||||
"A-j" | "A-down" => shrink_selection,
|
|
||||||
"A-h" | "A-left" => select_prev_sibling,
|
|
||||||
"A-l" | "A-right" => select_next_sibling,
|
|
||||||
|
|
||||||
"%" => select_all,
|
|
||||||
"x" => extend_line,
|
|
||||||
"X" => extend_to_line_bounds,
|
|
||||||
// crop_to_whole_line
|
|
||||||
|
|
||||||
"m" => { "Match"
|
|
||||||
"m" => match_brackets,
|
|
||||||
"s" => surround_add,
|
|
||||||
"r" => surround_replace,
|
|
||||||
"d" => surround_delete,
|
|
||||||
"a" => select_textobject_around,
|
|
||||||
"i" => select_textobject_inner,
|
|
||||||
},
|
|
||||||
"[" => { "Left bracket"
|
|
||||||
"d" => goto_prev_diag,
|
|
||||||
"D" => goto_first_diag,
|
|
||||||
"f" => goto_prev_function,
|
|
||||||
"c" => goto_prev_class,
|
|
||||||
"a" => goto_prev_parameter,
|
|
||||||
"o" => goto_prev_comment,
|
|
||||||
"space" => add_newline_above,
|
|
||||||
},
|
|
||||||
"]" => { "Right bracket"
|
|
||||||
"d" => goto_next_diag,
|
|
||||||
"D" => goto_last_diag,
|
|
||||||
"f" => goto_next_function,
|
|
||||||
"c" => goto_next_class,
|
|
||||||
"a" => goto_next_parameter,
|
|
||||||
"o" => goto_next_comment,
|
|
||||||
"space" => add_newline_below,
|
|
||||||
},
|
|
||||||
|
|
||||||
"/" => search,
|
|
||||||
"?" => rsearch,
|
|
||||||
"n" => search_next,
|
|
||||||
"N" => search_prev,
|
|
||||||
"*" => search_selection,
|
|
||||||
|
|
||||||
"u" => undo,
|
|
||||||
"U" => redo,
|
|
||||||
"A-u" => earlier,
|
|
||||||
"A-U" => later,
|
|
||||||
|
|
||||||
"y" => yank,
|
|
||||||
// yank_all
|
|
||||||
"p" => paste_after,
|
|
||||||
// paste_all
|
|
||||||
"P" => paste_before,
|
|
||||||
|
|
||||||
"Q" => record_macro,
|
|
||||||
"q" => replay_macro,
|
|
||||||
|
|
||||||
">" => indent,
|
|
||||||
"<" => unindent,
|
|
||||||
"=" => format_selections,
|
|
||||||
"J" => join_selections,
|
|
||||||
"K" => keep_selections,
|
|
||||||
"A-K" => remove_selections,
|
|
||||||
|
|
||||||
"," => keep_primary_selection,
|
|
||||||
"A-," => remove_primary_selection,
|
|
||||||
|
|
||||||
// "q" => record_macro,
|
|
||||||
// "Q" => replay_macro,
|
|
||||||
|
|
||||||
"&" => align_selections,
|
|
||||||
"_" => trim_selections,
|
|
||||||
|
|
||||||
"(" => rotate_selections_backward,
|
|
||||||
")" => rotate_selections_forward,
|
|
||||||
"A-(" => rotate_selection_contents_backward,
|
|
||||||
"A-)" => rotate_selection_contents_forward,
|
|
||||||
|
|
||||||
"A-:" => ensure_selections_forward,
|
|
||||||
|
|
||||||
"esc" => normal_mode,
|
|
||||||
"C-b" | "pageup" => page_up,
|
|
||||||
"C-f" | "pagedown" => page_down,
|
|
||||||
"C-u" => half_page_up,
|
|
||||||
"C-d" => half_page_down,
|
|
||||||
|
|
||||||
"C-w" => { "Window"
|
|
||||||
"C-w" | "w" => rotate_view,
|
|
||||||
"C-s" | "s" => hsplit,
|
|
||||||
"C-v" | "v" => vsplit,
|
|
||||||
"f" => goto_file_hsplit,
|
|
||||||
"F" => goto_file_vsplit,
|
|
||||||
"C-q" | "q" => wclose,
|
|
||||||
"C-o" | "o" => wonly,
|
|
||||||
"C-h" | "h" | "left" => jump_view_left,
|
|
||||||
"C-j" | "j" | "down" => jump_view_down,
|
|
||||||
"C-k" | "k" | "up" => jump_view_up,
|
|
||||||
"C-l" | "l" | "right" => jump_view_right,
|
|
||||||
"n" => { "New split scratch buffer"
|
|
||||||
"C-s" | "s" => hsplit_new,
|
|
||||||
"C-v" | "v" => vsplit_new,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// move under <space>c
|
|
||||||
"C-c" => toggle_comments,
|
|
||||||
|
|
||||||
// z family for save/restore/combine from/to sels from register
|
|
||||||
|
|
||||||
"tab" => jump_forward, // tab == <C-i>
|
|
||||||
"C-o" => jump_backward,
|
|
||||||
"C-s" => save_selection,
|
|
||||||
|
|
||||||
"space" => { "Space"
|
|
||||||
"f" => file_picker,
|
|
||||||
"b" => buffer_picker,
|
|
||||||
"s" => symbol_picker,
|
|
||||||
"S" => workspace_symbol_picker,
|
|
||||||
"a" => code_action,
|
|
||||||
"'" => last_picker,
|
|
||||||
"d" => { "Debug (experimental)" sticky=true
|
|
||||||
"l" => dap_launch,
|
|
||||||
"b" => dap_toggle_breakpoint,
|
|
||||||
"c" => dap_continue,
|
|
||||||
"h" => dap_pause,
|
|
||||||
"i" => dap_step_in,
|
|
||||||
"o" => dap_step_out,
|
|
||||||
"n" => dap_next,
|
|
||||||
"v" => dap_variables,
|
|
||||||
"t" => dap_terminate,
|
|
||||||
"C-c" => dap_edit_condition,
|
|
||||||
"C-l" => dap_edit_log,
|
|
||||||
"s" => { "Switch"
|
|
||||||
"t" => dap_switch_thread,
|
|
||||||
"f" => dap_switch_stack_frame,
|
|
||||||
// sl, sb
|
|
||||||
},
|
|
||||||
"e" => dap_enable_exceptions,
|
|
||||||
"E" => dap_disable_exceptions,
|
|
||||||
},
|
|
||||||
"w" => { "Window"
|
|
||||||
"C-w" | "w" => rotate_view,
|
|
||||||
"C-s" | "s" => hsplit,
|
|
||||||
"C-v" | "v" => vsplit,
|
|
||||||
"f" => goto_file_hsplit,
|
|
||||||
"F" => goto_file_vsplit,
|
|
||||||
"C-q" | "q" => wclose,
|
|
||||||
"C-o" | "o" => wonly,
|
|
||||||
"C-h" | "h" | "left" => jump_view_left,
|
|
||||||
"C-j" | "j" | "down" => jump_view_down,
|
|
||||||
"C-k" | "k" | "up" => jump_view_up,
|
|
||||||
"C-l" | "l" | "right" => jump_view_right,
|
|
||||||
"n" => { "New split scratch buffer"
|
|
||||||
"C-s" | "s" => hsplit_new,
|
|
||||||
"C-v" | "v" => vsplit_new,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"y" => yank_joined_to_clipboard,
|
|
||||||
"Y" => yank_main_selection_to_clipboard,
|
|
||||||
"p" => paste_clipboard_after,
|
|
||||||
"P" => paste_clipboard_before,
|
|
||||||
"R" => replace_selections_with_clipboard,
|
|
||||||
"/" => global_search,
|
|
||||||
"k" => hover,
|
|
||||||
"r" => rename_symbol,
|
|
||||||
"?" => command_palette,
|
|
||||||
},
|
|
||||||
"z" => { "View"
|
|
||||||
"z" | "c" => align_view_center,
|
|
||||||
"t" => align_view_top,
|
|
||||||
"b" => align_view_bottom,
|
|
||||||
"m" => align_view_middle,
|
|
||||||
"k" | "up" => scroll_up,
|
|
||||||
"j" | "down" => scroll_down,
|
|
||||||
"C-b" | "pageup" => page_up,
|
|
||||||
"C-f" | "pagedown" => page_down,
|
|
||||||
"C-u" => half_page_up,
|
|
||||||
"C-d" => half_page_down,
|
|
||||||
},
|
|
||||||
"Z" => { "View" sticky=true
|
|
||||||
"z" | "c" => align_view_center,
|
|
||||||
"t" => align_view_top,
|
|
||||||
"b" => align_view_bottom,
|
|
||||||
"m" => align_view_middle,
|
|
||||||
"k" | "up" => scroll_up,
|
|
||||||
"j" | "down" => scroll_down,
|
|
||||||
"C-b" | "pageup" => page_up,
|
|
||||||
"C-f" | "pagedown" => page_down,
|
|
||||||
"C-u" => half_page_up,
|
|
||||||
"C-d" => half_page_down,
|
|
||||||
},
|
|
||||||
|
|
||||||
"\"" => select_register,
|
|
||||||
"|" => shell_pipe,
|
|
||||||
"A-|" => shell_pipe_to,
|
|
||||||
"!" => shell_insert_output,
|
|
||||||
"A-!" => shell_append_output,
|
|
||||||
"$" => shell_keep_pipe,
|
|
||||||
"C-z" => suspend,
|
|
||||||
|
|
||||||
"C-a" => increment,
|
|
||||||
"C-x" => decrement,
|
|
||||||
});
|
|
||||||
let mut select = normal.clone();
|
|
||||||
select.merge_nodes(keymap!({ "Select mode"
|
|
||||||
"h" | "left" => extend_char_left,
|
|
||||||
"j" | "down" => extend_line_down,
|
|
||||||
"k" | "up" => extend_line_up,
|
|
||||||
"l" | "right" => extend_char_right,
|
|
||||||
|
|
||||||
"w" => extend_next_word_start,
|
|
||||||
"b" => extend_prev_word_start,
|
|
||||||
"e" => extend_next_word_end,
|
|
||||||
"W" => extend_next_long_word_start,
|
|
||||||
"B" => extend_prev_long_word_start,
|
|
||||||
"E" => extend_next_long_word_end,
|
|
||||||
|
|
||||||
"n" => extend_search_next,
|
|
||||||
"N" => extend_search_prev,
|
|
||||||
|
|
||||||
"t" => extend_till_char,
|
|
||||||
"f" => extend_next_char,
|
|
||||||
"T" => extend_till_prev_char,
|
|
||||||
"F" => extend_prev_char,
|
|
||||||
|
|
||||||
"home" => extend_to_line_start,
|
|
||||||
"end" => extend_to_line_end,
|
|
||||||
"esc" => exit_select_mode,
|
|
||||||
|
|
||||||
"v" => normal_mode,
|
|
||||||
}));
|
|
||||||
let insert = keymap!({ "Insert mode"
|
|
||||||
"esc" => normal_mode,
|
|
||||||
|
|
||||||
"backspace" => delete_char_backward,
|
|
||||||
"C-h" => delete_char_backward,
|
|
||||||
"del" => delete_char_forward,
|
|
||||||
"C-d" => delete_char_forward,
|
|
||||||
"ret" => insert_newline,
|
|
||||||
"C-j" => insert_newline,
|
|
||||||
"tab" => insert_tab,
|
|
||||||
"C-w" => delete_word_backward,
|
|
||||||
"A-backspace" => delete_word_backward,
|
|
||||||
"A-d" => delete_word_forward,
|
|
||||||
|
|
||||||
"left" => move_char_left,
|
|
||||||
"C-b" => move_char_left,
|
|
||||||
"down" => move_line_down,
|
|
||||||
"C-n" => move_line_down,
|
|
||||||
"up" => move_line_up,
|
|
||||||
"C-p" => move_line_up,
|
|
||||||
"right" => move_char_right,
|
|
||||||
"C-f" => move_char_right,
|
|
||||||
"A-b" => move_prev_word_end,
|
|
||||||
"A-left" => move_prev_word_end,
|
|
||||||
"A-f" => move_next_word_start,
|
|
||||||
"A-right" => move_next_word_start,
|
|
||||||
"A-<" => goto_file_start,
|
|
||||||
"A->" => goto_file_end,
|
|
||||||
"pageup" => page_up,
|
|
||||||
"pagedown" => page_down,
|
|
||||||
"home" => goto_line_start,
|
|
||||||
"C-a" => goto_line_start,
|
|
||||||
"end" => goto_line_end_newline,
|
|
||||||
"C-e" => goto_line_end_newline,
|
|
||||||
|
|
||||||
"C-k" => kill_to_line_end,
|
|
||||||
"C-u" => kill_to_line_start,
|
|
||||||
|
|
||||||
"C-x" => completion,
|
|
||||||
"C-r" => insert_register,
|
|
||||||
});
|
|
||||||
Self::new(hashmap!(
|
|
||||||
Mode::Normal => Keymap::new(normal),
|
|
||||||
Mode::Select => Keymap::new(select),
|
|
||||||
Mode::Insert => Keymap::new(insert),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge default config keys with user overwritten keys for custom user config.
|
/// Merge default config keys with user overwritten keys for custom user config.
|
||||||
pub fn merge_keys(mut config: Config) -> Config {
|
pub fn merge_keys(mut config: Config) -> Config {
|
||||||
let mut delta = std::mem::take(&mut config.keys);
|
let mut delta = std::mem::replace(&mut config.keys, default());
|
||||||
for (mode, keys) in &mut config.keys.map {
|
for (mode, keys) in &mut config.keys {
|
||||||
keys.merge(delta.map.remove(mode).unwrap_or_default())
|
keys.merge(delta.remove(mode).unwrap_or_default())
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::macros::keymap;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use arc_swap::access::Constant;
|
||||||
|
use helix_core::hashmap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
|
@ -855,7 +395,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_partial_keys() {
|
fn merge_partial_keys() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
keys: Keymaps::new(hashmap! {
|
keys: hashmap! {
|
||||||
Mode::Normal => Keymap::new(
|
Mode::Normal => Keymap::new(
|
||||||
keymap!({ "Normal mode"
|
keymap!({ "Normal mode"
|
||||||
"i" => normal_mode,
|
"i" => normal_mode,
|
||||||
|
@ -867,13 +407,13 @@ mod tests {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}),
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut merged_config = merge_keys(config.clone());
|
let mut merged_config = merge_keys(config.clone());
|
||||||
assert_ne!(config, merged_config);
|
assert_ne!(config, merged_config);
|
||||||
|
|
||||||
let keymap = &mut merged_config.keys;
|
let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone())));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keymap.get(Mode::Normal, key!('i')),
|
keymap.get(Mode::Normal, key!('i')),
|
||||||
KeymapResult::Matched(MappableCommand::normal_mode),
|
KeymapResult::Matched(MappableCommand::normal_mode),
|
||||||
|
@ -891,7 +431,7 @@ mod tests {
|
||||||
"Leaf should replace node"
|
"Leaf should replace node"
|
||||||
);
|
);
|
||||||
|
|
||||||
let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap();
|
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
|
||||||
// Assumes that `g` is a node in default keymap
|
// Assumes that `g` is a node in default keymap
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
|
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
|
||||||
|
@ -911,14 +451,14 @@ mod tests {
|
||||||
"Old leaves in subnode should be present in merged node"
|
"Old leaves in subnode should be present in merged node"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(merged_config.keys.map.get(&Mode::Normal).unwrap().len() > 1);
|
assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1);
|
||||||
assert!(merged_config.keys.map.get(&Mode::Insert).unwrap().len() > 0);
|
assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn order_should_be_set() {
|
fn order_should_be_set() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
keys: Keymaps::new(hashmap! {
|
keys: hashmap! {
|
||||||
Mode::Normal => Keymap::new(
|
Mode::Normal => Keymap::new(
|
||||||
keymap!({ "Normal mode"
|
keymap!({ "Normal mode"
|
||||||
"space" => { ""
|
"space" => { ""
|
||||||
|
@ -929,12 +469,12 @@ mod tests {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}),
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut merged_config = merge_keys(config.clone());
|
let mut merged_config = merge_keys(config.clone());
|
||||||
assert_ne!(config, merged_config);
|
assert_ne!(config, merged_config);
|
||||||
let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap();
|
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
|
||||||
// Make sure mapping works
|
// Make sure mapping works
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keymap
|
keymap
|
||||||
|
@ -951,8 +491,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aliased_modes_are_same_in_default_keymap() {
|
fn aliased_modes_are_same_in_default_keymap() {
|
||||||
let keymaps = Keymaps::default();
|
let keymaps = Keymaps::default().map();
|
||||||
let root = keymaps.map.get(&Mode::Normal).unwrap().root();
|
let root = keymaps.get(&Mode::Normal).unwrap().root();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
root.search(&[key!(' '), key!('w')]).unwrap(),
|
root.search(&[key!(' '), key!('w')]).unwrap(),
|
||||||
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
|
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
|
||||||
|
|
359
helix-term/src/keymap/default.rs
Normal file
359
helix-term/src/keymap/default.rs
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::macros::keymap;
|
||||||
|
use super::{Keymap, Mode};
|
||||||
|
use helix_core::hashmap;
|
||||||
|
|
||||||
|
pub fn default() -> HashMap<Mode, Keymap> {
|
||||||
|
let normal = keymap!({ "Normal mode"
|
||||||
|
"h" | "left" => move_char_left,
|
||||||
|
"j" | "down" => move_line_down,
|
||||||
|
"k" | "up" => move_line_up,
|
||||||
|
"l" | "right" => move_char_right,
|
||||||
|
|
||||||
|
"t" => find_till_char,
|
||||||
|
"f" => find_next_char,
|
||||||
|
"T" => till_prev_char,
|
||||||
|
"F" => find_prev_char,
|
||||||
|
"r" => replace,
|
||||||
|
"R" => replace_with_yanked,
|
||||||
|
"A-." => repeat_last_motion,
|
||||||
|
|
||||||
|
"~" => switch_case,
|
||||||
|
"`" => switch_to_lowercase,
|
||||||
|
"A-`" => switch_to_uppercase,
|
||||||
|
|
||||||
|
"home" => goto_line_start,
|
||||||
|
"end" => goto_line_end,
|
||||||
|
|
||||||
|
"w" => move_next_word_start,
|
||||||
|
"b" => move_prev_word_start,
|
||||||
|
"e" => move_next_word_end,
|
||||||
|
|
||||||
|
"W" => move_next_long_word_start,
|
||||||
|
"B" => move_prev_long_word_start,
|
||||||
|
"E" => move_next_long_word_end,
|
||||||
|
|
||||||
|
"v" => select_mode,
|
||||||
|
"G" => goto_line,
|
||||||
|
"g" => { "Goto"
|
||||||
|
"g" => goto_file_start,
|
||||||
|
"e" => goto_last_line,
|
||||||
|
"f" => goto_file,
|
||||||
|
"h" => goto_line_start,
|
||||||
|
"l" => goto_line_end,
|
||||||
|
"s" => goto_first_nonwhitespace,
|
||||||
|
"d" => goto_definition,
|
||||||
|
"y" => goto_type_definition,
|
||||||
|
"r" => goto_reference,
|
||||||
|
"i" => goto_implementation,
|
||||||
|
"t" => goto_window_top,
|
||||||
|
"c" => goto_window_center,
|
||||||
|
"b" => goto_window_bottom,
|
||||||
|
"a" => goto_last_accessed_file,
|
||||||
|
"m" => goto_last_modified_file,
|
||||||
|
"n" => goto_next_buffer,
|
||||||
|
"p" => goto_previous_buffer,
|
||||||
|
"." => goto_last_modification,
|
||||||
|
},
|
||||||
|
":" => command_mode,
|
||||||
|
|
||||||
|
"i" => insert_mode,
|
||||||
|
"I" => prepend_to_line,
|
||||||
|
"a" => append_mode,
|
||||||
|
"A" => append_to_line,
|
||||||
|
"o" => open_below,
|
||||||
|
"O" => open_above,
|
||||||
|
|
||||||
|
"d" => delete_selection,
|
||||||
|
"A-d" => delete_selection_noyank,
|
||||||
|
"c" => change_selection,
|
||||||
|
"A-c" => change_selection_noyank,
|
||||||
|
|
||||||
|
"C" => copy_selection_on_next_line,
|
||||||
|
"A-C" => copy_selection_on_prev_line,
|
||||||
|
|
||||||
|
|
||||||
|
"s" => select_regex,
|
||||||
|
"A-s" => split_selection_on_newline,
|
||||||
|
"S" => split_selection,
|
||||||
|
";" => collapse_selection,
|
||||||
|
"A-;" => flip_selections,
|
||||||
|
"A-k" | "A-up" => expand_selection,
|
||||||
|
"A-j" | "A-down" => shrink_selection,
|
||||||
|
"A-h" | "A-left" => select_prev_sibling,
|
||||||
|
"A-l" | "A-right" => select_next_sibling,
|
||||||
|
|
||||||
|
"%" => select_all,
|
||||||
|
"x" => extend_line,
|
||||||
|
"X" => extend_to_line_bounds,
|
||||||
|
// crop_to_whole_line
|
||||||
|
|
||||||
|
"m" => { "Match"
|
||||||
|
"m" => match_brackets,
|
||||||
|
"s" => surround_add,
|
||||||
|
"r" => surround_replace,
|
||||||
|
"d" => surround_delete,
|
||||||
|
"a" => select_textobject_around,
|
||||||
|
"i" => select_textobject_inner,
|
||||||
|
},
|
||||||
|
"[" => { "Left bracket"
|
||||||
|
"d" => goto_prev_diag,
|
||||||
|
"D" => goto_first_diag,
|
||||||
|
"f" => goto_prev_function,
|
||||||
|
"c" => goto_prev_class,
|
||||||
|
"a" => goto_prev_parameter,
|
||||||
|
"o" => goto_prev_comment,
|
||||||
|
"space" => add_newline_above,
|
||||||
|
},
|
||||||
|
"]" => { "Right bracket"
|
||||||
|
"d" => goto_next_diag,
|
||||||
|
"D" => goto_last_diag,
|
||||||
|
"f" => goto_next_function,
|
||||||
|
"c" => goto_next_class,
|
||||||
|
"a" => goto_next_parameter,
|
||||||
|
"o" => goto_next_comment,
|
||||||
|
"space" => add_newline_below,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/" => search,
|
||||||
|
"?" => rsearch,
|
||||||
|
"n" => search_next,
|
||||||
|
"N" => search_prev,
|
||||||
|
"*" => search_selection,
|
||||||
|
|
||||||
|
"u" => undo,
|
||||||
|
"U" => redo,
|
||||||
|
"A-u" => earlier,
|
||||||
|
"A-U" => later,
|
||||||
|
|
||||||
|
"y" => yank,
|
||||||
|
// yank_all
|
||||||
|
"p" => paste_after,
|
||||||
|
// paste_all
|
||||||
|
"P" => paste_before,
|
||||||
|
|
||||||
|
"Q" => record_macro,
|
||||||
|
"q" => replay_macro,
|
||||||
|
|
||||||
|
">" => indent,
|
||||||
|
"<" => unindent,
|
||||||
|
"=" => format_selections,
|
||||||
|
"J" => join_selections,
|
||||||
|
"K" => keep_selections,
|
||||||
|
"A-K" => remove_selections,
|
||||||
|
|
||||||
|
"," => keep_primary_selection,
|
||||||
|
"A-," => remove_primary_selection,
|
||||||
|
|
||||||
|
// "q" => record_macro,
|
||||||
|
// "Q" => replay_macro,
|
||||||
|
|
||||||
|
"&" => align_selections,
|
||||||
|
"_" => trim_selections,
|
||||||
|
|
||||||
|
"(" => rotate_selections_backward,
|
||||||
|
")" => rotate_selections_forward,
|
||||||
|
"A-(" => rotate_selection_contents_backward,
|
||||||
|
"A-)" => rotate_selection_contents_forward,
|
||||||
|
|
||||||
|
"A-:" => ensure_selections_forward,
|
||||||
|
|
||||||
|
"esc" => normal_mode,
|
||||||
|
"C-b" | "pageup" => page_up,
|
||||||
|
"C-f" | "pagedown" => page_down,
|
||||||
|
"C-u" => half_page_up,
|
||||||
|
"C-d" => half_page_down,
|
||||||
|
|
||||||
|
"C-w" => { "Window"
|
||||||
|
"C-w" | "w" => rotate_view,
|
||||||
|
"C-s" | "s" => hsplit,
|
||||||
|
"C-v" | "v" => vsplit,
|
||||||
|
"f" => goto_file_hsplit,
|
||||||
|
"F" => goto_file_vsplit,
|
||||||
|
"C-q" | "q" => wclose,
|
||||||
|
"C-o" | "o" => wonly,
|
||||||
|
"C-h" | "h" | "left" => jump_view_left,
|
||||||
|
"C-j" | "j" | "down" => jump_view_down,
|
||||||
|
"C-k" | "k" | "up" => jump_view_up,
|
||||||
|
"C-l" | "l" | "right" => jump_view_right,
|
||||||
|
"n" => { "New split scratch buffer"
|
||||||
|
"C-s" | "s" => hsplit_new,
|
||||||
|
"C-v" | "v" => vsplit_new,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// move under <space>c
|
||||||
|
"C-c" => toggle_comments,
|
||||||
|
|
||||||
|
// z family for save/restore/combine from/to sels from register
|
||||||
|
|
||||||
|
"tab" => jump_forward, // tab == <C-i>
|
||||||
|
"C-o" => jump_backward,
|
||||||
|
"C-s" => save_selection,
|
||||||
|
|
||||||
|
"space" => { "Space"
|
||||||
|
"f" => file_picker,
|
||||||
|
"b" => buffer_picker,
|
||||||
|
"s" => symbol_picker,
|
||||||
|
"S" => workspace_symbol_picker,
|
||||||
|
"a" => code_action,
|
||||||
|
"'" => last_picker,
|
||||||
|
"d" => { "Debug (experimental)" sticky=true
|
||||||
|
"l" => dap_launch,
|
||||||
|
"b" => dap_toggle_breakpoint,
|
||||||
|
"c" => dap_continue,
|
||||||
|
"h" => dap_pause,
|
||||||
|
"i" => dap_step_in,
|
||||||
|
"o" => dap_step_out,
|
||||||
|
"n" => dap_next,
|
||||||
|
"v" => dap_variables,
|
||||||
|
"t" => dap_terminate,
|
||||||
|
"C-c" => dap_edit_condition,
|
||||||
|
"C-l" => dap_edit_log,
|
||||||
|
"s" => { "Switch"
|
||||||
|
"t" => dap_switch_thread,
|
||||||
|
"f" => dap_switch_stack_frame,
|
||||||
|
// sl, sb
|
||||||
|
},
|
||||||
|
"e" => dap_enable_exceptions,
|
||||||
|
"E" => dap_disable_exceptions,
|
||||||
|
},
|
||||||
|
"w" => { "Window"
|
||||||
|
"C-w" | "w" => rotate_view,
|
||||||
|
"C-s" | "s" => hsplit,
|
||||||
|
"C-v" | "v" => vsplit,
|
||||||
|
"f" => goto_file_hsplit,
|
||||||
|
"F" => goto_file_vsplit,
|
||||||
|
"C-q" | "q" => wclose,
|
||||||
|
"C-o" | "o" => wonly,
|
||||||
|
"C-h" | "h" | "left" => jump_view_left,
|
||||||
|
"C-j" | "j" | "down" => jump_view_down,
|
||||||
|
"C-k" | "k" | "up" => jump_view_up,
|
||||||
|
"C-l" | "l" | "right" => jump_view_right,
|
||||||
|
"n" => { "New split scratch buffer"
|
||||||
|
"C-s" | "s" => hsplit_new,
|
||||||
|
"C-v" | "v" => vsplit_new,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"y" => yank_joined_to_clipboard,
|
||||||
|
"Y" => yank_main_selection_to_clipboard,
|
||||||
|
"p" => paste_clipboard_after,
|
||||||
|
"P" => paste_clipboard_before,
|
||||||
|
"R" => replace_selections_with_clipboard,
|
||||||
|
"/" => global_search,
|
||||||
|
"k" => hover,
|
||||||
|
"r" => rename_symbol,
|
||||||
|
"?" => command_palette,
|
||||||
|
},
|
||||||
|
"z" => { "View"
|
||||||
|
"z" | "c" => align_view_center,
|
||||||
|
"t" => align_view_top,
|
||||||
|
"b" => align_view_bottom,
|
||||||
|
"m" => align_view_middle,
|
||||||
|
"k" | "up" => scroll_up,
|
||||||
|
"j" | "down" => scroll_down,
|
||||||
|
"C-b" | "pageup" => page_up,
|
||||||
|
"C-f" | "pagedown" => page_down,
|
||||||
|
"C-u" => half_page_up,
|
||||||
|
"C-d" => half_page_down,
|
||||||
|
},
|
||||||
|
"Z" => { "View" sticky=true
|
||||||
|
"z" | "c" => align_view_center,
|
||||||
|
"t" => align_view_top,
|
||||||
|
"b" => align_view_bottom,
|
||||||
|
"m" => align_view_middle,
|
||||||
|
"k" | "up" => scroll_up,
|
||||||
|
"j" | "down" => scroll_down,
|
||||||
|
"C-b" | "pageup" => page_up,
|
||||||
|
"C-f" | "pagedown" => page_down,
|
||||||
|
"C-u" => half_page_up,
|
||||||
|
"C-d" => half_page_down,
|
||||||
|
},
|
||||||
|
|
||||||
|
"\"" => select_register,
|
||||||
|
"|" => shell_pipe,
|
||||||
|
"A-|" => shell_pipe_to,
|
||||||
|
"!" => shell_insert_output,
|
||||||
|
"A-!" => shell_append_output,
|
||||||
|
"$" => shell_keep_pipe,
|
||||||
|
"C-z" => suspend,
|
||||||
|
|
||||||
|
"C-a" => increment,
|
||||||
|
"C-x" => decrement,
|
||||||
|
});
|
||||||
|
let mut select = normal.clone();
|
||||||
|
select.merge_nodes(keymap!({ "Select mode"
|
||||||
|
"h" | "left" => extend_char_left,
|
||||||
|
"j" | "down" => extend_line_down,
|
||||||
|
"k" | "up" => extend_line_up,
|
||||||
|
"l" | "right" => extend_char_right,
|
||||||
|
|
||||||
|
"w" => extend_next_word_start,
|
||||||
|
"b" => extend_prev_word_start,
|
||||||
|
"e" => extend_next_word_end,
|
||||||
|
"W" => extend_next_long_word_start,
|
||||||
|
"B" => extend_prev_long_word_start,
|
||||||
|
"E" => extend_next_long_word_end,
|
||||||
|
|
||||||
|
"n" => extend_search_next,
|
||||||
|
"N" => extend_search_prev,
|
||||||
|
|
||||||
|
"t" => extend_till_char,
|
||||||
|
"f" => extend_next_char,
|
||||||
|
"T" => extend_till_prev_char,
|
||||||
|
"F" => extend_prev_char,
|
||||||
|
|
||||||
|
"home" => extend_to_line_start,
|
||||||
|
"end" => extend_to_line_end,
|
||||||
|
"esc" => exit_select_mode,
|
||||||
|
|
||||||
|
"v" => normal_mode,
|
||||||
|
}));
|
||||||
|
let insert = keymap!({ "Insert mode"
|
||||||
|
"esc" => normal_mode,
|
||||||
|
|
||||||
|
"backspace" => delete_char_backward,
|
||||||
|
"C-h" => delete_char_backward,
|
||||||
|
"del" => delete_char_forward,
|
||||||
|
"C-d" => delete_char_forward,
|
||||||
|
"ret" => insert_newline,
|
||||||
|
"C-j" => insert_newline,
|
||||||
|
"tab" => insert_tab,
|
||||||
|
"C-w" => delete_word_backward,
|
||||||
|
"A-backspace" => delete_word_backward,
|
||||||
|
"A-d" => delete_word_forward,
|
||||||
|
|
||||||
|
"left" => move_char_left,
|
||||||
|
"C-b" => move_char_left,
|
||||||
|
"down" => move_line_down,
|
||||||
|
"C-n" => move_line_down,
|
||||||
|
"up" => move_line_up,
|
||||||
|
"C-p" => move_line_up,
|
||||||
|
"right" => move_char_right,
|
||||||
|
"C-f" => move_char_right,
|
||||||
|
"A-b" => move_prev_word_end,
|
||||||
|
"A-left" => move_prev_word_end,
|
||||||
|
"A-f" => move_next_word_start,
|
||||||
|
"A-right" => move_next_word_start,
|
||||||
|
"A-<" => goto_file_start,
|
||||||
|
"A->" => goto_file_end,
|
||||||
|
"pageup" => page_up,
|
||||||
|
"pagedown" => page_down,
|
||||||
|
"home" => goto_line_start,
|
||||||
|
"C-a" => goto_line_start,
|
||||||
|
"end" => goto_line_end_newline,
|
||||||
|
"C-e" => goto_line_end_newline,
|
||||||
|
|
||||||
|
"C-k" => kill_to_line_end,
|
||||||
|
"C-u" => kill_to_line_start,
|
||||||
|
|
||||||
|
"C-x" => completion,
|
||||||
|
"C-r" => insert_register,
|
||||||
|
});
|
||||||
|
hashmap!(
|
||||||
|
Mode::Normal => Keymap::new(normal),
|
||||||
|
Mode::Select => Keymap::new(select),
|
||||||
|
Mode::Insert => Keymap::new(insert),
|
||||||
|
)
|
||||||
|
}
|
127
helix-term/src/keymap/macros.rs
Normal file
127
helix-term/src/keymap/macros.rs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! key {
|
||||||
|
($key:ident) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::$key,
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($($ch:tt)*) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! shift {
|
||||||
|
($key:ident) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::$key,
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($($ch:tt)*) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! ctrl {
|
||||||
|
($key:ident) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::$key,
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($($ch:tt)*) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! alt {
|
||||||
|
($key:ident) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::$key,
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($($ch:tt)*) => {
|
||||||
|
::helix_view::input::KeyEvent {
|
||||||
|
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
||||||
|
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro for defining the root of a `Keymap` object. Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use helix_core::hashmap;
|
||||||
|
/// # use helix_term::keymap;
|
||||||
|
/// # use helix_term::keymap::Keymap;
|
||||||
|
/// let normal_mode = keymap!({ "Normal mode"
|
||||||
|
/// "i" => insert_mode,
|
||||||
|
/// "g" => { "Goto"
|
||||||
|
/// "g" => goto_file_start,
|
||||||
|
/// "e" => goto_file_end,
|
||||||
|
/// },
|
||||||
|
/// "j" | "down" => move_line_down,
|
||||||
|
/// });
|
||||||
|
/// let keymap = Keymap::new(normal_mode);
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! keymap {
|
||||||
|
(@trie $cmd:ident) => {
|
||||||
|
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
|
||||||
|
};
|
||||||
|
|
||||||
|
(@trie
|
||||||
|
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
|
||||||
|
) => {
|
||||||
|
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
|
||||||
|
};
|
||||||
|
|
||||||
|
(@trie [$($cmd:ident),* $(,)?]) => {
|
||||||
|
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
|
||||||
|
) => {
|
||||||
|
// modified from the hashmap! macro
|
||||||
|
{
|
||||||
|
let _cap = hashmap!(@count $($($key),+),*);
|
||||||
|
let mut _map = ::std::collections::HashMap::with_capacity(_cap);
|
||||||
|
let mut _order = ::std::vec::Vec::with_capacity(_cap);
|
||||||
|
$(
|
||||||
|
$(
|
||||||
|
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
|
||||||
|
let _duplicate = _map.insert(
|
||||||
|
_key,
|
||||||
|
keymap!(@trie $value)
|
||||||
|
);
|
||||||
|
assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
|
||||||
|
_order.push(_key);
|
||||||
|
)+
|
||||||
|
)*
|
||||||
|
let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
|
||||||
|
$( _node.is_sticky = $sticky; )?
|
||||||
|
$crate::keymap::KeyTrie::Node(_node)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use alt;
|
||||||
|
pub use ctrl;
|
||||||
|
pub use key;
|
||||||
|
pub use keymap;
|
||||||
|
pub use shift;
|
|
@ -10,6 +10,7 @@ pub mod health;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub use keymap::macros::*;
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn true_color() -> bool {
|
fn true_color() -> bool {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use anyhow::{Context, Error, Result};
|
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, ConfigLoadError};
|
||||||
use helix_term::keymap::merge_keys;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||||
|
@ -60,7 +59,6 @@ ARGS:
|
||||||
|
|
||||||
FLAGS:
|
FLAGS:
|
||||||
-h, --help Prints help information
|
-h, --help Prints help information
|
||||||
--edit-config Opens the helix config file
|
|
||||||
--tutor Loads the tutorial
|
--tutor Loads the tutorial
|
||||||
--health [LANG] Checks for potential errors in editor setup
|
--health [LANG] Checks for potential errors in editor setup
|
||||||
If given, checks for config errors in language LANG
|
If given, checks for config errors in language LANG
|
||||||
|
@ -118,19 +116,24 @@ FLAGS:
|
||||||
std::fs::create_dir_all(&conf_dir).ok();
|
std::fs::create_dir_all(&conf_dir).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = match std::fs::read_to_string(helix_loader::config_file()) {
|
let config = match Config::load_default() {
|
||||||
Ok(config) => toml::from_str(&config)
|
Ok(config) => config,
|
||||||
.map(merge_keys)
|
Err(err) => {
|
||||||
.unwrap_or_else(|err| {
|
match err {
|
||||||
eprintln!("Bad config: {}", err);
|
ConfigLoadError::BadConfig(err) => {
|
||||||
eprintln!("Press <ENTER> to continue with default config");
|
eprintln!("Bad config: {}", err);
|
||||||
use std::io::Read;
|
eprintln!("Press <ENTER> to continue with default config");
|
||||||
// This waits for an enter press.
|
use std::io::Read;
|
||||||
let _ = std::io::stdin().read(&mut []);
|
// This waits for an enter press.
|
||||||
Config::default()
|
let _ = std::io::stdin().read(&mut []);
|
||||||
}),
|
Config::default()
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
|
}
|
||||||
Err(err) => return Err(Error::new(err)),
|
ConfigLoadError::Error(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
Config::default()
|
||||||
|
}
|
||||||
|
ConfigLoadError::Error(err) => return Err(Error::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||||
|
|
|
@ -118,7 +118,7 @@ impl EditorView {
|
||||||
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
|
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
|
||||||
Box::new(syntax::merge(
|
Box::new(syntax::merge(
|
||||||
highlights,
|
highlights,
|
||||||
Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape),
|
Self::doc_selection_highlights(doc, view, theme, &editor.config().cursor_shape),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Box::new(highlights)
|
Box::new(highlights)
|
||||||
|
@ -702,7 +702,6 @@ impl EditorView {
|
||||||
cxt: &mut commands::Context,
|
cxt: &mut commands::Context,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) -> Option<KeymapResult> {
|
) -> Option<KeymapResult> {
|
||||||
cxt.editor.autoinfo = None;
|
|
||||||
let key_result = self.keymaps.get(mode, event);
|
let key_result = self.keymaps.get(mode, event);
|
||||||
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
|
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
|
||||||
|
|
||||||
|
@ -845,7 +844,7 @@ impl EditorView {
|
||||||
|
|
||||||
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
|
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
|
||||||
if self.completion.is_some()
|
if self.completion.is_some()
|
||||||
|| !cx.editor.config.auto_completion
|
|| !cx.editor.config().auto_completion
|
||||||
|| doc!(cx.editor).mode != Mode::Insert
|
|| doc!(cx.editor).mode != Mode::Insert
|
||||||
{
|
{
|
||||||
return EventResult::Ignored(None);
|
return EventResult::Ignored(None);
|
||||||
|
@ -871,6 +870,7 @@ impl EditorView {
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
cxt: &mut commands::Context,
|
cxt: &mut commands::Context,
|
||||||
) -> EventResult {
|
) -> EventResult {
|
||||||
|
let config = cxt.editor.config();
|
||||||
match event {
|
match event {
|
||||||
MouseEvent {
|
MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
|
@ -971,7 +971,7 @@ impl EditorView {
|
||||||
None => return EventResult::Ignored(None),
|
None => return EventResult::Ignored(None),
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = cxt.editor.config.scroll_lines.abs() as usize;
|
let offset = config.scroll_lines.abs() as usize;
|
||||||
commands::scroll(cxt, offset, direction);
|
commands::scroll(cxt, offset, direction);
|
||||||
|
|
||||||
cxt.editor.tree.focus = current_view;
|
cxt.editor.tree.focus = current_view;
|
||||||
|
@ -983,7 +983,7 @@ impl EditorView {
|
||||||
kind: MouseEventKind::Up(MouseButton::Left),
|
kind: MouseEventKind::Up(MouseButton::Left),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if !cxt.editor.config.middle_click_paste {
|
if !config.middle_click_paste {
|
||||||
return EventResult::Ignored(None);
|
return EventResult::Ignored(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1039,7 +1039,7 @@ impl EditorView {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let editor = &mut cxt.editor;
|
let editor = &mut cxt.editor;
|
||||||
if !editor.config.middle_click_paste {
|
if !config.middle_click_paste {
|
||||||
return EventResult::Ignored(None);
|
return EventResult::Ignored(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1163,9 +1163,9 @@ impl Component for EditorView {
|
||||||
if cx.editor.should_close() {
|
if cx.editor.should_close() {
|
||||||
return EventResult::Ignored(None);
|
return EventResult::Ignored(None);
|
||||||
}
|
}
|
||||||
|
let config = cx.editor.config();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
|
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||||
|
|
||||||
// Store a history state if not in insert mode. This also takes care of
|
// Store a history state if not in insert mode. This also takes care of
|
||||||
// commiting changes when leaving insert mode.
|
// commiting changes when leaving insert mode.
|
||||||
|
@ -1206,7 +1206,7 @@ impl Component for EditorView {
|
||||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||||
// clear with background color
|
// clear with background color
|
||||||
surface.set_style(area, cx.editor.theme.get("ui.background"));
|
surface.set_style(area, cx.editor.theme.get("ui.background"));
|
||||||
|
let config = cx.editor.config();
|
||||||
// if the terminal size suddenly changed, we need to trigger a resize
|
// if the terminal size suddenly changed, we need to trigger a resize
|
||||||
cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
|
cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
|
||||||
|
|
||||||
|
@ -1215,7 +1215,7 @@ impl Component for EditorView {
|
||||||
self.render_view(cx.editor, doc, view, area, surface, is_focused);
|
self.render_view(cx.editor, doc, view, area, surface, is_focused);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cx.editor.config.auto_info {
|
if config.auto_info {
|
||||||
if let Some(mut info) = cx.editor.autoinfo.take() {
|
if let Some(mut info) = cx.editor.autoinfo.take() {
|
||||||
info.render(area, surface, cx);
|
info.render(area, surface, cx);
|
||||||
cx.editor.autoinfo = Some(info)
|
cx.editor.autoinfo = Some(info)
|
||||||
|
|
|
@ -37,6 +37,7 @@ pub fn regex_prompt(
|
||||||
let doc_id = view.doc;
|
let doc_id = view.doc;
|
||||||
let snapshot = doc.selection(view.id).clone();
|
let snapshot = doc.selection(view.id).clone();
|
||||||
let offset_snapshot = view.offset;
|
let offset_snapshot = view.offset;
|
||||||
|
let config = cx.editor.config();
|
||||||
|
|
||||||
let mut prompt = Prompt::new(
|
let mut prompt = Prompt::new(
|
||||||
prompt,
|
prompt,
|
||||||
|
@ -65,7 +66,7 @@ pub fn regex_prompt(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let case_insensitive = if cx.editor.config.search.smart_case {
|
let case_insensitive = if config.search.smart_case {
|
||||||
!input.chars().any(char::is_uppercase)
|
!input.chars().any(char::is_uppercase)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -84,7 +85,7 @@ pub fn regex_prompt(
|
||||||
|
|
||||||
fun(view, doc, regex, event);
|
fun(view, doc, regex, event);
|
||||||
|
|
||||||
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
|
view.ensure_cursor_in_view(doc, config.scrolloff);
|
||||||
}
|
}
|
||||||
Err(_err) => (), // TODO: mark command line as error
|
Err(_err) => (), // TODO: mark command line as error
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,16 @@ term = ["crossterm"]
|
||||||
bitflags = "1.3"
|
bitflags = "1.3"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
helix-core = { version = "0.6", path = "../helix-core" }
|
helix-core = { version = "0.6", path = "../helix-core" }
|
||||||
helix-lsp = { version = "0.6", path = "../helix-lsp"}
|
helix-lsp = { version = "0.6", path = "../helix-lsp" }
|
||||||
helix-dap = { version = "0.6", path = "../helix-dap"}
|
helix-dap = { version = "0.6", path = "../helix-dap" }
|
||||||
crossterm = { version = "0.23", optional = true }
|
crossterm = { version = "0.23", optional = true }
|
||||||
|
|
||||||
# Conversion traits
|
# Conversion traits
|
||||||
once_cell = "1.10"
|
once_cell = "1.10"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
|
||||||
|
arc-swap = { version = "1.5.0" }
|
||||||
|
|
||||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||||
|
|
|
@ -13,7 +13,6 @@ use futures_util::future;
|
||||||
use futures_util::stream::select_all::SelectAll;
|
use futures_util::stream::select_all::SelectAll;
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
|
@ -24,7 +23,10 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::time::{sleep, Duration, Instant, Sleep};
|
use tokio::{
|
||||||
|
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||||
|
time::{sleep, Duration, Instant, Sleep},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, Error};
|
||||||
|
|
||||||
|
@ -40,6 +42,8 @@ use helix_dap as dap;
|
||||||
|
|
||||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
use arc_swap::access::{DynAccess, DynGuard};
|
||||||
|
|
||||||
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
|
@ -287,7 +291,6 @@ pub struct Breakpoint {
|
||||||
pub log_message: Option<String>,
|
pub log_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
pub tree: Tree,
|
pub tree: Tree,
|
||||||
pub next_document_id: DocumentId,
|
pub next_document_id: DocumentId,
|
||||||
|
@ -311,7 +314,7 @@ pub struct Editor {
|
||||||
pub status_msg: Option<(Cow<'static, str>, Severity)>,
|
pub status_msg: Option<(Cow<'static, str>, Severity)>,
|
||||||
pub autoinfo: Option<Info>,
|
pub autoinfo: Option<Info>,
|
||||||
|
|
||||||
pub config: Config,
|
pub config: Box<dyn DynAccess<Config>>,
|
||||||
pub auto_pairs: Option<AutoPairs>,
|
pub auto_pairs: Option<AutoPairs>,
|
||||||
|
|
||||||
pub idle_timer: Pin<Box<Sleep>>,
|
pub idle_timer: Pin<Box<Sleep>>,
|
||||||
|
@ -321,6 +324,14 @@ pub struct Editor {
|
||||||
pub last_completion: Option<CompleteAction>,
|
pub last_completion: Option<CompleteAction>,
|
||||||
|
|
||||||
pub exit_code: i32,
|
pub exit_code: i32,
|
||||||
|
|
||||||
|
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ConfigEvent {
|
||||||
|
Refresh,
|
||||||
|
Update(Config),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -342,12 +353,11 @@ impl Editor {
|
||||||
mut area: Rect,
|
mut area: Rect,
|
||||||
theme_loader: Arc<theme::Loader>,
|
theme_loader: Arc<theme::Loader>,
|
||||||
syn_loader: Arc<syntax::Loader>,
|
syn_loader: Arc<syntax::Loader>,
|
||||||
config: Config,
|
config: Box<dyn DynAccess<Config>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let language_servers = helix_lsp::Registry::new();
|
let language_servers = helix_lsp::Registry::new();
|
||||||
let auto_pairs = (&config.auto_pairs).into();
|
let conf = config.load();
|
||||||
|
let auto_pairs = (&conf.auto_pairs).into();
|
||||||
debug!("Editor config: {config:#?}");
|
|
||||||
|
|
||||||
// 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
|
||||||
area.height -= 1;
|
area.height -= 1;
|
||||||
|
@ -370,16 +380,21 @@ impl Editor {
|
||||||
clipboard_provider: get_clipboard_provider(),
|
clipboard_provider: get_clipboard_provider(),
|
||||||
status_msg: None,
|
status_msg: None,
|
||||||
autoinfo: None,
|
autoinfo: None,
|
||||||
idle_timer: Box::pin(sleep(config.idle_timeout)),
|
idle_timer: Box::pin(sleep(conf.idle_timeout)),
|
||||||
last_motion: None,
|
last_motion: None,
|
||||||
last_completion: None,
|
last_completion: None,
|
||||||
pseudo_pending: None,
|
pseudo_pending: None,
|
||||||
config,
|
config,
|
||||||
auto_pairs,
|
auto_pairs,
|
||||||
exit_code: 0,
|
exit_code: 0,
|
||||||
|
config_events: unbounded_channel(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config(&self) -> DynGuard<Config> {
|
||||||
|
self.config.load()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_idle_timer(&mut self) {
|
pub fn clear_idle_timer(&mut self) {
|
||||||
// equivalent to internal Instant::far_future() (30 years)
|
// equivalent to internal Instant::far_future() (30 years)
|
||||||
self.idle_timer
|
self.idle_timer
|
||||||
|
@ -388,9 +403,10 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_idle_timer(&mut self) {
|
pub fn reset_idle_timer(&mut self) {
|
||||||
|
let config = self.config();
|
||||||
self.idle_timer
|
self.idle_timer
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.reset(Instant::now() + self.config.idle_timeout);
|
.reset(Instant::now() + config.idle_timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_status(&mut self) {
|
pub fn clear_status(&mut self) {
|
||||||
|
@ -466,9 +482,10 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _refresh(&mut self) {
|
fn _refresh(&mut self) {
|
||||||
|
let config = self.config();
|
||||||
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];
|
||||||
view.ensure_cursor_in_view(doc, self.config.scrolloff)
|
view.ensure_cursor_in_view(doc, config.scrolloff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,9 +733,10 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
|
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
|
||||||
|
let config = self.config();
|
||||||
let view = self.tree.get_mut(id);
|
let view = self.tree.get_mut(id);
|
||||||
let doc = &self.documents[&view.doc];
|
let doc = &self.documents[&view.doc];
|
||||||
view.ensure_cursor_in_view(doc, self.config.scrolloff)
|
view.ensure_cursor_in_view(doc, config.scrolloff)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -752,6 +770,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
||||||
|
let config = self.config();
|
||||||
let (view, doc) = current_ref!(self);
|
let (view, doc) = current_ref!(self);
|
||||||
let cursor = doc
|
let cursor = doc
|
||||||
.selection(view.id)
|
.selection(view.id)
|
||||||
|
@ -761,7 +780,7 @@ impl Editor {
|
||||||
let inner = view.inner_area();
|
let inner = view.inner_area();
|
||||||
pos.col += inner.x as usize;
|
pos.col += inner.x as usize;
|
||||||
pos.row += inner.y as usize;
|
pos.row += inner.y as usize;
|
||||||
let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
|
let cursorkind = config.cursor_shape.from_mode(doc.mode());
|
||||||
(Some(pos), cursorkind)
|
(Some(pos), cursorkind)
|
||||||
} else {
|
} else {
|
||||||
(None, CursorKind::default())
|
(None, CursorKind::default())
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub fn line_number<'doc>(
|
||||||
.text()
|
.text()
|
||||||
.char_to_line(doc.selection(view.id).primary().cursor(text));
|
.char_to_line(doc.selection(view.id).primary().cursor(text));
|
||||||
|
|
||||||
let config = editor.config.line_number;
|
let line_number = editor.config().line_number;
|
||||||
let mode = doc.mode;
|
let mode = doc.mode;
|
||||||
|
|
||||||
Box::new(move |line: usize, selected: bool, out: &mut String| {
|
Box::new(move |line: usize, selected: bool, out: &mut String| {
|
||||||
|
@ -70,7 +70,7 @@ pub fn line_number<'doc>(
|
||||||
} else {
|
} else {
|
||||||
use crate::{document::Mode, editor::LineNumber};
|
use crate::{document::Mode, editor::LineNumber};
|
||||||
|
|
||||||
let relative = config == LineNumber::Relative
|
let relative = line_number == LineNumber::Relative
|
||||||
&& mode != Mode::Insert
|
&& mode != Mode::Insert
|
||||||
&& is_focused
|
&& is_focused
|
||||||
&& current_line != line;
|
&& current_line != line;
|
||||||
|
|
Loading…
Add table
Reference in a new issue