diff --git a/Cargo.lock b/Cargo.lock index c2fc99829..0e9ba36a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,24 +878,17 @@ dependencies = [ "anyhow", "arc-swap", "chrono", - "content_inspector", "crossterm", "fern", "futures-util", - "fuzzy-matcher", - "grep-regex", - "grep-searcher", "helix-core", "helix-dap", "helix-loader", "helix-lsp", "helix-tui", "helix-view", - "ignore", "log", "once_cell", - "pulldown-cmark", - "retain_mut", "ropey", "serde", "serde_json", @@ -942,15 +935,23 @@ dependencies = [ "bitflags", "chardetng", "clipboard-win", + "content_inspector", "crossterm", "futures-util", + "fuzzy-matcher", + "grep-regex", + "grep-searcher", "helix-core", "helix-dap", "helix-graphics", + "helix-loader", "helix-lsp", "helix-tui", + "ignore", "log", "once_cell", + "pulldown-cmark", + "retain_mut", "serde", "serde_json", "slotmap", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index cd4973ad3..7b38d42d4 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -51,27 +51,12 @@ fern = "0.6" chrono = { version = "0.4", default-features = false, features = ["clock"] } log = "0.4" -# File picker -fuzzy-matcher = "0.3" -ignore = "0.4" -# markdown doc rendering -pulldown-cmark = { version = "0.9", default-features = false } -# file type detection -content_inspector = "0.2.4" - # config toml = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -# ripgrep for global search -grep-regex = "0.1.9" -grep-searcher = "0.1.8" - -# Remove once retain_mut lands in stable rust -retain_mut = "0.1.7" - [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index cc5ff0c4c..8f7a4ae62 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -4,18 +4,20 @@ use helix_core::{ pos_at_coords, syntax, Selection, }; -#[cfg(feature = "lsp")] -use crate::commands::apply_workspace_edit; #[cfg(feature = "lsp")] use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; #[cfg(feature = "lsp")] +use helix_view::commands::apply_workspace_edit; +#[cfg(feature = "lsp")] use serde_json::json; -use helix_view::{align_view, editor::ConfigEvent, graphics::Rect, theme, Align, Editor}; +use helix_view::{ + align_view, editor::ConfigEvent, graphics::Rect, theme, true_color, Align, Editor, +}; + +use crate::{args::Args, config::Config}; -use crate::{ - args::Args, - config::Config, +use helix_view::{ keymap::Keymaps, ui::{self, overlay::overlayed}, }; @@ -96,7 +98,7 @@ impl Application { &helix_loader::runtime_dir(), )); - let true_color = config.editor.true_color || crate::true_color(); + let true_color = config.editor.true_color || true_color(); let theme = config .theme .as_ref() @@ -358,7 +360,7 @@ impl Application { } fn true_color(&self) -> bool { - self.config.load().editor.true_color || crate::true_color() + self.config.load().editor.true_color || true_color() } #[cfg(windows)] diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index b99c7d1a2..e1f0d9a71 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -1,6 +1,7 @@ use anyhow::Result; use helix_core::Position; -use std::path::{Path, PathBuf}; +use helix_view::args::parse_file; +use std::path::PathBuf; #[derive(Default)] pub struct Args { @@ -65,37 +66,3 @@ impl Args { Ok(args) } } - -/// Parse arg into [`PathBuf`] and position. -pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) { - let def = || (PathBuf::from(s), Position::default()); - if Path::new(s).exists() { - return def(); - } - split_path_row_col(s) - .or_else(|| split_path_row(s)) - .unwrap_or_else(def) -} - -/// Split file.rs:10:2 into [`PathBuf`], row and col. -/// -/// Does not validate if file.rs is a file or directory. -fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> { - let mut s = s.rsplitn(3, ':'); - let col: usize = s.next()?.parse().ok()?; - let row: usize = s.next()?.parse().ok()?; - let path = s.next()?.into(); - let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); - Some((path, pos)) -} - -/// Split file.rs:10 into [`PathBuf`] and row. -/// -/// Does not validate if file.rs is a file or directory. -fn split_path_row(s: &str) -> Option<(PathBuf, Position)> { - let (path, row) = s.rsplit_once(':')?; - let row: usize = row.parse().ok()?; - let path = path.into(); - let pos = Position::new(row.saturating_sub(1), 0); - Some((path, pos)) -} diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f..27dcb9711 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,5 +1,5 @@ -use crate::keymap::{default::default, merge_keys, Keymap}; use helix_view::document::Mode; +use helix_view::keymap::{default::default, Keymap}; use serde::Deserialize; use std::collections::HashMap; use std::fmt::Display; @@ -27,6 +27,15 @@ impl Default for Config { } } +/// Merge default config keys with user overwritten keys for custom user config. +pub fn merge_keys(mut config: Config) -> Config { + let mut delta = std::mem::replace(&mut config.keys, default()); + for (mode, keys) in &mut config.keys { + keys.merge(delta.remove(mode).unwrap_or_default()) + } + config +} + #[derive(Debug)] pub enum ConfigLoadError { BadConfig(TomlError), @@ -63,10 +72,9 @@ mod tests { #[test] fn parsing_keymaps_config_file() { - use crate::keymap; - use crate::keymap::Keymap; use helix_core::hashmap; use helix_view::document::Mode; + use helix_view::keymap::{self, Keymap}; let sample_keymaps = r#" [keys.insert] @@ -104,4 +112,104 @@ mod tests { let default_keys = Config::default().keys; assert_eq!(default_keys, default()); } + + use arc_swap::access::Constant; + use helix_core::hashmap; + + #[test] + fn merge_partial_keys() { + let config = Config { + keys: hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "i" => normal_mode, + "无" => insert_mode, + "z" => jump_backward, + "g" => { "Merge into goto mode" + "$" => goto_line_end, + "g" => delete_char_forward, + }, + }) + ) + }, + ..Default::default() + }; + let mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + + let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + assert_eq!( + keymap.get(Mode::Normal, key!('i')), + KeymapResult::Matched(MappableCommand::normal_mode), + "Leaf should replace leaf" + ); + assert_eq!( + keymap.get(Mode::Normal, key!('无')), + KeymapResult::Matched(MappableCommand::insert_mode), + "New leaf should be present in merged keymap" + ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(Mode::Normal, key!('z')), + KeymapResult::Matched(MappableCommand::jump_backward), + "Leaf should replace node" + ); + + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('$')]).unwrap(), + &KeyTrie::Leaf(MappableCommand::goto_line_end), + "Leaf should be present in merged subnode" + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('g')]).unwrap(), + &KeyTrie::Leaf(MappableCommand::delete_char_forward), + "Leaf should replace old leaf in merged subnode" + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('e')]).unwrap(), + &KeyTrie::Leaf(MappableCommand::goto_last_line), + "Old leaves in subnode should be present in merged node" + ); + + assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + } + + #[test] + fn order_should_be_set() { + let config = Config { + keys: hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, + }, + }, + }) + ) + }, + ..Default::default() + }; + let mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Make sure mapping works + assert_eq!( + keymap + .root() + .search(&[key!(' '), key!('s'), key!('v')]) + .unwrap(), + &KeyTrie::Leaf(MappableCommand::vsplit), + "Leaf should be present in merged subnode" + ); + // Make sure an order was set during merge + let node = keymap.root().search(&[helix_view::key!(' ')]).unwrap(); + assert!(!node.node().unwrap().order().is_empty()) + } } diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs deleted file mode 100644 index c4a1bfbb3..000000000 --- a/helix-term/src/keymap/macros.rs +++ /dev/null @@ -1,127 +0,0 @@ -#[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; diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index d7f527859..891653f79 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -3,20 +3,5 @@ extern crate helix_view; pub mod application; pub mod args; -pub mod commands; pub mod config; pub mod health; -pub mod keymap; -pub mod ui; -pub use keymap::macros::*; - -#[cfg(not(windows))] -fn true_color() -> bool { - std::env::var("COLORTERM") - .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) - .unwrap_or(false) -} -#[cfg(windows)] -fn true_color() -> bool { - true -} diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 15478e81a..69df49ccf 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -23,6 +23,7 @@ helix-core = { version = "0.6", path = "../helix-core" } helix-graphics = { version = "0.6", path = "../helix-graphics" } helix-lsp = { version = "0.6", path = "../helix-lsp", optional = true } helix-dap = { version = "0.6", path = "../helix-dap", optional = true } +helix-loader = { version = "0.6", path = "../helix-loader" } tokio-stream = { version = "0.1", optional = true } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"], optional = true } @@ -46,6 +47,23 @@ serde_json = "1.0" toml = "0.5" log = "~0.4" +# Command dependencies + +# File picker +fuzzy-matcher = "0.3" +ignore = "0.4" +# markdown doc rendering +pulldown-cmark = { version = "0.9", default-features = false } +# file type detection +content_inspector = "0.2.4" + +# ripgrep for global search +grep-regex = "0.1.9" +grep-searcher = "0.1.8" + +# Remove once retain_mut lands in stable rust +retain_mut = "0.1.7" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] which = "4.2" diff --git a/helix-view/src/args.rs b/helix-view/src/args.rs new file mode 100644 index 000000000..4a69e12de --- /dev/null +++ b/helix-view/src/args.rs @@ -0,0 +1,36 @@ +use helix_core::Position; +use std::path::{Path, PathBuf}; + +/// Parse arg into [`PathBuf`] and position. +pub fn parse_file(s: &str) -> (PathBuf, Position) { + let def = || (PathBuf::from(s), Position::default()); + if Path::new(s).exists() { + return def(); + } + split_path_row_col(s) + .or_else(|| split_path_row(s)) + .unwrap_or_else(def) +} + +/// Split file.rs:10:2 into [`PathBuf`], row and col. +/// +/// Does not validate if file.rs is a file or directory. +fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> { + let mut s = s.rsplitn(3, ':'); + let col: usize = s.next()?.parse().ok()?; + let row: usize = s.next()?.parse().ok()?; + let path = s.next()?.into(); + let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); + Some((path, pos)) +} + +/// Split file.rs:10 into [`PathBuf`] and row. +/// +/// Does not validate if file.rs is a file or directory. +fn split_path_row(s: &str) -> Option<(PathBuf, Position)> { + let (path, row) = s.rsplit_once(':')?; + let row: usize = row.parse().ok()?; + let path = path.into(); + let pos = Position::new(row.saturating_sub(1), 0); + Some((path, pos)) +} diff --git a/helix-term/src/commands/dap.rs b/helix-view/src/commands/dap.rs similarity index 99% rename from helix-term/src/commands/dap.rs rename to helix-view/src/commands/dap.rs index 2ef8b5ff0..65e4fa062 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-view/src/commands/dap.rs @@ -1,13 +1,13 @@ use super::{Context, Editor}; +use crate::editor::Breakpoint; use crate::ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}; -use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion}; -use helix_dap::{self as dap, Client}; -use helix_lsp::block_on; -use helix_view::editor::Breakpoint; -use helix_view::{ +use crate::{ compositor::{self, Compositor}, job::{Callback, Jobs}, }; +use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion}; +use helix_dap::{self as dap, Client}; +use helix_lsp::block_on; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -18,7 +18,8 @@ use std::path::PathBuf; use anyhow::{anyhow, bail}; -use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id}; +use crate::debugger; +use crate::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id}; fn thread_picker( cx: &mut Context, @@ -474,7 +475,7 @@ pub fn dap_variables(cx: &mut Context) { let text_style = theme.get("ui.text.focus"); for scope in scopes.iter() { - // use helix_view::graphics::Style; + // use crate::graphics::Style; use tui::text::{Span, Spans}; let response = block_on(debugger.variables(scope.variables_reference)); diff --git a/helix-term/src/commands/lsp.rs b/helix-view/src/commands/lsp.rs similarity index 99% rename from helix-term/src/commands/lsp.rs rename to helix-view/src/commands/lsp.rs index 3fafb03ee..816a653f2 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-view/src/commands/lsp.rs @@ -7,10 +7,10 @@ use helix_lsp::{ use super::{align_view, push_jump, Align, Context, Editor}; use helix_core::Selection; -use helix_view::editor::Action; +use crate::editor::Action; use crate::ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent}; -use helix_view::compositor::{self, Compositor}; +use crate::compositor::{self, Compositor}; use std::borrow::Cow; @@ -255,7 +255,7 @@ pub fn code_action(cx: &mut Context) { }); picker.move_down(); // pre-select the first item - let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin { + let popup = Popup::new("code-action", picker).margin(crate::graphics::Margin { vertical: 1, horizontal: 1, }); diff --git a/helix-term/src/commands.rs b/helix-view/src/commands/mod.rs similarity index 99% rename from helix-term/src/commands.rs rename to helix-view/src/commands/mod.rs index ac2266767..5aa29ba4b 100644 --- a/helix-term/src/commands.rs +++ b/helix-view/src/commands/mod.rs @@ -10,6 +10,16 @@ pub use dap::*; pub use lsp::*; pub use typed::*; +use crate::{ + clipboard::ClipboardType, + document::{Mode, SCRATCH_BUFFER_NAME}, + editor::{Action, Motion}, + info::Info, + input::KeyEvent, + keyboard::KeyCode, + view::View, + Document, DocumentId, Editor, ViewId, +}; use helix_core::{ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, @@ -29,18 +39,8 @@ use helix_core::{ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; -use helix_view::{ - clipboard::ClipboardType, - document::{Mode, SCRATCH_BUFFER_NAME}, - editor::{Action, Motion}, - info::Info, - input::KeyEvent, - keyboard::KeyCode, - view::View, - Document, DocumentId, Editor, ViewId, -}; -use helix_view::{ +use crate::{ compositor::{self, Component, Compositor}, job::{self, Job, Jobs}, }; @@ -56,8 +56,13 @@ use crate::{ }; use futures_util::{FutureExt, StreamExt}; -use std::{collections::HashMap, fmt, future::Future}; -use std::{collections::HashSet, num::NonZeroUsize}; + +use std::{ + collections::{HashMap, HashSet}, + fmt, + future::Future, + num::NonZeroUsize, +}; use std::{ borrow::Cow, @@ -77,7 +82,7 @@ pub struct Context<'a> { pub count: Option, pub editor: &'a mut Editor, - pub callback: Option, + pub callback: Option, pub on_next_key_callback: Option>, pub jobs: &'a mut Jobs, } @@ -126,7 +131,7 @@ impl<'a> Context<'a> { } } -use helix_view::{align_view, Align}; +use crate::{align_view, Align}; /// A MappableCommand is either a static command like "jump_view_up" or a Typable command like /// :format. It causes a side-effect on the state (usually by creating and applying a transaction). @@ -4397,8 +4402,8 @@ fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBeha } fn suspend(_cx: &mut Context) { - #[cfg(not(windows))] - signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); + // #[cfg(not(windows))] + // signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); } fn add_newline_above(cx: &mut Context) { @@ -4557,7 +4562,7 @@ fn record_macro(cx: &mut Context) { fn replay_macro(cx: &mut Context) { let reg = cx.register.unwrap_or('@'); let keys: Vec = if let Some([keys_str]) = cx.editor.registers.read(reg) { - match helix_view::input::parse_macro(keys_str) { + match crate::input::parse_macro(keys_str) { Ok(keys) => keys, Err(err) => { cx.editor.set_error(format!("Invalid macro: {}", err)); diff --git a/helix-term/src/commands/typed.rs b/helix-view/src/commands/typed.rs similarity index 99% rename from helix-term/src/commands/typed.rs rename to helix-view/src/commands/typed.rs index d3e185c79..aeed6a56f 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-view/src/commands/typed.rs @@ -1,6 +1,6 @@ use super::*; -use helix_view::editor::{Action, ConfigEvent}; +use crate::editor::{Action, ConfigEvent}; use ui::completers::{self, Completer}; #[derive(Clone)] diff --git a/helix-term/src/keymap.rs b/helix-view/src/keymap.rs similarity index 75% rename from helix-term/src/keymap.rs rename to helix-view/src/keymap.rs index 37dbc5de2..c9f26e17e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-view/src/keymap.rs @@ -2,12 +2,11 @@ pub mod default; pub mod macros; pub use crate::commands::MappableCommand; -use crate::config::Config; +use crate::{document::Mode, info::Info, input::KeyEvent}; use arc_swap::{ access::{DynAccess, DynGuard}, ArcSwap, }; -use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ borrow::Cow, @@ -361,20 +360,10 @@ impl Default for Keymaps { } } -/// Merge default config keys with user overwritten keys for custom user config. -pub fn merge_keys(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default()); - for (mode, keys) in &mut config.keys { - keys.merge(delta.remove(mode).unwrap_or_default()) - } - config -} - #[cfg(test)] mod tests { use super::macros::keymap; use super::*; - use arc_swap::access::Constant; use helix_core::hashmap; #[test] @@ -392,103 +381,6 @@ mod tests { Keymaps::default(); } - #[test] - fn merge_partial_keys() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "i" => normal_mode, - "无" => insert_mode, - "z" => jump_backward, - "g" => { "Merge into goto mode" - "$" => goto_line_end, - "g" => delete_char_forward, - }, - }) - ) - }, - ..Default::default() - }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); - assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "Leaf should replace leaf" - ); - assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New leaf should be present in merged keymap" - ); - // Assumes that z is a node in the default keymap - assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "Leaf should replace node" - ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_line_end), - "Leaf should be present in merged subnode" - ); - // Assumes that `gg` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::delete_char_forward), - "Leaf should replace old leaf in merged subnode" - ); - // Assumes that `ge` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_last_line), - "Old leaves in subnode should be present in merged node" - ); - - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); - } - - #[test] - fn order_should_be_set() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, - }, - }) - ) - }, - ..Default::default() - }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Make sure mapping works - assert_eq!( - keymap - .root() - .search(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - &KeyTrie::Leaf(MappableCommand::vsplit), - "Leaf should be present in merged subnode" - ); - // Make sure an order was set during merge - let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order().is_empty()) - } - #[test] fn aliased_modes_are_same_in_default_keymap() { let keymaps = Keymaps::default().map(); diff --git a/helix-term/src/keymap/default.rs b/helix-view/src/keymap/default.rs similarity index 100% rename from helix-term/src/keymap/default.rs rename to helix-view/src/keymap/default.rs diff --git a/helix-view/src/keymap/macros.rs b/helix-view/src/keymap/macros.rs new file mode 100644 index 000000000..1f6e2aa74 --- /dev/null +++ b/helix-view/src/keymap/macros.rs @@ -0,0 +1,60 @@ +/// 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::<$crate::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 crate::{alt, ctrl, key, shift}; +pub use keymap; diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index b85196504..5ba31853d 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -1,17 +1,22 @@ #[macro_use] pub mod macros; +pub mod args; pub mod backend { #[cfg(feature = "term")] pub mod term; } pub mod clipboard; +pub mod commands; pub mod compositor; pub mod document; pub mod editor; +pub mod ui; pub use helix_graphics as graphics; pub mod gutter; pub mod job; +pub mod keymap; +pub use keymap::macros::*; pub mod handlers { #[cfg(feature = "dap")] pub mod dap; @@ -47,6 +52,17 @@ slotmap::new_key_type! { pub struct ViewId; } +#[cfg(not(windows))] +pub fn true_color() -> bool { + std::env::var("COLORTERM") + .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) + .unwrap_or(false) +} +#[cfg(windows)] +pub fn true_color() -> bool { + true +} + pub enum Align { Top, Center, diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index 04f8df944..f939d129d 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -61,3 +61,69 @@ macro_rules! doc { $crate::current_ref!($editor).1 }}; } + +// Keymap macros + +#[macro_export] +macro_rules! key { + ($key:ident) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::$key, + modifiers: $crate::keyboard::KeyModifiers::NONE, + } + }; + ($($ch:tt)*) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::Char($($ch)*), + modifiers: $crate::keyboard::KeyModifiers::NONE, + } + }; +} + +#[macro_export] +macro_rules! shift { + ($key:ident) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::$key, + modifiers: $crate::keyboard::KeyModifiers::SHIFT, + } + }; + ($($ch:tt)*) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::Char($($ch)*), + modifiers: $crate::keyboard::KeyModifiers::SHIFT, + } + }; +} + +#[macro_export] +macro_rules! ctrl { + ($key:ident) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::$key, + modifiers: $crate::keyboard::KeyModifiers::CONTROL, + } + }; + ($($ch:tt)*) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::Char($($ch)*), + modifiers: $crate::keyboard::KeyModifiers::CONTROL, + } + }; +} + +#[macro_export] +macro_rules! alt { + ($key:ident) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::$key, + modifiers: $crate::keyboard::KeyModifiers::ALT, + } + }; + ($($ch:tt)*) => { + $crate::input::KeyEvent { + code: $crate::keyboard::KeyCode::Char($($ch)*), + modifiers: $crate::keyboard::KeyModifiers::ALT, + } + }; +} diff --git a/helix-term/src/ui/completion.rs b/helix-view/src/ui/completion.rs similarity index 99% rename from helix-term/src/ui/completion.rs rename to helix-view/src/ui/completion.rs index b144a78b8..64ef686e8 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-view/src/ui/completion.rs @@ -1,10 +1,10 @@ -use helix_view::compositor::{Component, Context, Event, EventResult, RenderContext}; -use helix_view::editor::CompleteAction; +use crate::compositor::{Component, Context, Event, EventResult, RenderContext}; +use crate::editor::CompleteAction; use std::borrow::Cow; use helix_core::{Change, Transaction}; -use helix_view::{ +use crate::{ graphics::Rect, input::{KeyCode, KeyEvent}, Document, Editor, diff --git a/helix-term/src/ui/editor.rs b/helix-view/src/ui/editor.rs similarity index 99% rename from helix-term/src/ui/editor.rs rename to helix-view/src/ui/editor.rs index e6f4c1096..8d479337f 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-view/src/ui/editor.rs @@ -4,7 +4,7 @@ use crate::{ ui::{Completion, ProgressSpinners}, }; -use helix_view::compositor::{Component, Context, Event, EventResult, RenderContext}; +use crate::compositor::{Component, Context, Event, EventResult, RenderContext}; use helix_core::{ coords_at_pos, encoding, @@ -17,7 +17,7 @@ use helix_core::{ unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, Transaction, }; -use helix_view::{ +use crate::{ document::{Mode, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{CursorKind, Modifier, Rect, Style}, @@ -928,7 +928,7 @@ impl EditorView { editor.clear_idle_timer(); // don't retrigger } - pub fn handle_idle_timeout(&mut self, cx: &mut helix_view::compositor::Context) -> EventResult { + pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { if self.completion.is_some() || !cx.editor.config().auto_completion || doc!(cx.editor).mode != Mode::Insert @@ -1163,7 +1163,7 @@ impl Component for EditorView { fn handle_event( &mut self, event: Event, - context: &mut helix_view::compositor::Context, + context: &mut crate::compositor::Context, ) -> EventResult { let mut cx = commands::Context { editor: context.editor, @@ -1314,7 +1314,7 @@ impl Component for EditorView { // render status msg if let Some((status_msg, severity)) = &cx.editor.status_msg { status_msg_width = status_msg.width(); - use helix_view::editor::Severity; + use crate::editor::Severity; let style = if *severity == Severity::Error { cx.editor.theme.get("error") } else { @@ -1361,7 +1361,7 @@ impl Component for EditorView { if let Some((reg, _)) = cx.editor.macro_recording { let disp = format!("[{}]", reg); let style = style - .fg(helix_view::graphics::Color::Yellow) + .fg(crate::graphics::Color::Yellow) .add_modifier(Modifier::BOLD); cx.surface.set_string( area.x + area.width.saturating_sub(3), diff --git a/helix-term/src/ui/markdown.rs b/helix-view/src/ui/markdown.rs similarity index 99% rename from helix-term/src/ui/markdown.rs rename to helix-view/src/ui/markdown.rs index 6ed08dd92..def84477c 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-view/src/ui/markdown.rs @@ -1,4 +1,4 @@ -use helix_view::compositor::{Component, RenderContext}; +use crate::compositor::{Component, RenderContext}; use tui::text::{Span, Spans, Text}; use std::sync::Arc; @@ -9,7 +9,7 @@ use helix_core::{ syntax::{self, HighlightEvent, Syntax}, Rope, }; -use helix_view::{ +use crate::{ graphics::{Margin, Rect, Style}, Theme, }; diff --git a/helix-term/src/ui/menu.rs b/helix-view/src/ui/menu.rs similarity index 99% rename from helix-term/src/ui/menu.rs rename to helix-view/src/ui/menu.rs index 8bc97d500..de370e0d8 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-view/src/ui/menu.rs @@ -1,5 +1,5 @@ use crate::{ctrl, key, shift}; -use helix_view::compositor::{ +use crate::compositor::{ Callback, Component, Compositor, Context, Event, EventResult, RenderContext, }; use tui::widgets::Table; @@ -9,7 +9,7 @@ pub use tui::widgets::{Cell, Row}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use helix_view::{graphics::Rect, Editor}; +use crate::{graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item { diff --git a/helix-term/src/ui/mod.rs b/helix-view/src/ui/mod.rs similarity index 96% rename from helix-term/src/ui/mod.rs rename to helix-view/src/ui/mod.rs index 7f7df2022..0105175e7 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-view/src/ui/mod.rs @@ -19,9 +19,9 @@ pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; +use crate::{Document, Editor, View}; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; -use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -30,7 +30,7 @@ pub fn prompt( prompt: std::borrow::Cow<'static, str>, history_register: Option, completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, - callback_fn: impl FnMut(&mut helix_view::compositor::Context, &str, PromptEvent) + 'static, + callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static, ) { let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn); // Calculate initial completion @@ -55,7 +55,7 @@ pub fn regex_prompt( prompt, history_register, completion_fn, - move |cx: &mut helix_view::compositor::Context, input: &str, event: PromptEvent| { + move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { PromptEvent::Abort => { let (view, doc) = current!(cx.editor); @@ -111,7 +111,7 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker(root: PathBuf, config: &crate::editor::Config) -> FilePicker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -188,12 +188,12 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub mod completers { + use crate::document::SCRATCH_BUFFER_NAME; + use crate::theme; use crate::ui::prompt::Completion; + use crate::{editor::Config, Editor}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; - use helix_view::document::SCRATCH_BUFFER_NAME; - use helix_view::theme; - use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; use std::cmp::Reverse; diff --git a/helix-term/src/ui/overlay.rs b/helix-view/src/ui/overlay.rs similarity index 95% rename from helix-term/src/ui/overlay.rs rename to helix-view/src/ui/overlay.rs index c0e52654b..c7e153d17 100644 --- a/helix-term/src/ui/overlay.rs +++ b/helix-view/src/ui/overlay.rs @@ -1,10 +1,10 @@ use helix_core::Position; -use helix_view::{ +use crate::{ graphics::{CursorKind, Rect}, Editor, }; -use helix_view::compositor::{Component, Context, Event, EventResult, RenderContext}; +use crate::compositor::{Component, Context, Event, EventResult, RenderContext}; /// Contains a component placed in the center of the parent component pub struct Overlay { diff --git a/helix-term/src/ui/picker.rs b/helix-view/src/ui/picker.rs similarity index 99% rename from helix-term/src/ui/picker.rs rename to helix-view/src/ui/picker.rs index c7381c997..2fbacab3e 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-view/src/ui/picker.rs @@ -2,7 +2,7 @@ use crate::{ ctrl, key, shift, ui::{self, EditorView}, }; -use helix_view::compositor::{Component, Compositor, Context, Event, EventResult, RenderContext}; +use crate::compositor::{Component, Compositor, Context, Event, EventResult, RenderContext}; use tui::widgets::{Block, BorderType, Borders}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; @@ -20,7 +20,7 @@ use std::{ use crate::ui::{Prompt, PromptEvent}; use helix_core::{movement::Direction, Position}; -use helix_view::{ +use crate::{ editor::Action, graphics::{Color, CursorKind, Margin, Modifier, Rect, Style}, Document, Editor, diff --git a/helix-term/src/ui/popup.rs b/helix-view/src/ui/popup.rs similarity index 98% rename from helix-term/src/ui/popup.rs rename to helix-view/src/ui/popup.rs index c58146e59..c8de7e74c 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-view/src/ui/popup.rs @@ -1,8 +1,8 @@ use crate::{ctrl, key}; -use helix_view::compositor::{Callback, Component, Context, Event, EventResult, RenderContext}; +use crate::compositor::{Callback, Component, Context, Event, EventResult, RenderContext}; use helix_core::Position; -use helix_view::{ +use crate::{ graphics::{Margin, Rect}, Editor, }; diff --git a/helix-term/src/ui/prompt.rs b/helix-view/src/ui/prompt.rs similarity index 99% rename from helix-term/src/ui/prompt.rs rename to helix-view/src/ui/prompt.rs index 1b85758ec..183d5de46 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-view/src/ui/prompt.rs @@ -1,17 +1,17 @@ -use helix_view::compositor::{Component, Compositor, Context, Event, EventResult, RenderContext}; +use crate::compositor::{Component, Compositor, Context, Event, EventResult, RenderContext}; +use crate::input::KeyEvent; +use crate::keyboard::KeyCode; use crate::{alt, ctrl, key, shift, ui}; -use helix_view::input::KeyEvent; -use helix_view::keyboard::KeyCode; use std::{borrow::Cow, ops::RangeFrom}; use tui::widgets::{Block, Borders, Widget}; -use helix_core::{ - unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, -}; -use helix_view::{ +use crate::{ graphics::{CursorKind, Margin, Rect}, Editor, }; +use helix_core::{ + unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, +}; pub type Completion = (RangeFrom, Cow<'static, str>); diff --git a/helix-term/src/ui/spinner.rs b/helix-view/src/ui/spinner.rs similarity index 100% rename from helix-term/src/ui/spinner.rs rename to helix-view/src/ui/spinner.rs diff --git a/helix-term/src/ui/text.rs b/helix-view/src/ui/text.rs similarity index 95% rename from helix-term/src/ui/text.rs rename to helix-view/src/ui/text.rs index 4097deb56..224aa2306 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-view/src/ui/text.rs @@ -1,6 +1,5 @@ -use helix_view::compositor::{Component, RenderContext}; - -use helix_view::graphics::Rect; +use crate::compositor::{Component, RenderContext}; +use crate::graphics::Rect; pub struct Text { pub(crate) contents: tui::text::Text<'static>,