From e177b27baf85611892b024453005808fed49707f Mon Sep 17 00:00:00 2001 From: Perry Thompson Date: Thu, 1 Jul 2021 21:52:22 -0500 Subject: [PATCH 01/28] Add missing import --- helix-core/src/syntax.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 9dbb2c03..5b45a88f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -94,6 +94,7 @@ fn load_runtime_file(language: &str, filename: &str) -> Result Result> { use std::fmt; + use std::path::PathBuf; #[derive(rust_embed::RustEmbed)] #[folder = "../runtime/"] From c5b2973739901f8cd4bc26f3cfc8232249eb7968 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Fri, 2 Jul 2021 10:54:50 -0400 Subject: [PATCH 02/28] `:reload` (#374) * reloading functionality * fn with_newline_eof() * fmt * wip * wip * wip * wip * moved to core, added simd feature for encoding_rs * wip * rm * .gitignore * wip * local wip * wip * wip * no features * wip * nit * remove simd * doc * clippy * clippy * address comments * add indentation & line ending change --- Cargo.lock | 35 +++++++++++++++++++ helix-core/Cargo.toml | 5 +++ helix-core/src/diff.rs | 70 ++++++++++++++++++++++++++++++++++++++ helix-core/src/lib.rs | 1 + helix-term/src/commands.rs | 32 +++++++++++++++++ helix-view/src/document.rs | 61 ++++++++++++++++++++++++++++----- 6 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 helix-core/src/diff.rs diff --git a/Cargo.lock b/Cargo.lock index 473ae8c8..a377e2f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,10 +317,12 @@ dependencies = [ "etcetera", "helix-syntax", "once_cell", + "quickcheck", "regex", "ropey", "rust-embed", "serde", + "similar", "smallvec", "tendril", "toml", @@ -692,6 +694,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "rand", +] + [[package]] name = "quote" version = "1.0.9" @@ -701,6 +712,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.9" @@ -872,6 +901,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + [[package]] name = "slab" version = "0.4.3" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 726e90cc..80d559a9 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -31,5 +31,10 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } toml = "0.5" +similar = "1.3" + etcetera = "0.3" rust-embed = { version = "5.9.0", optional = true } + +[dev-dependencies] +quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs new file mode 100644 index 00000000..9c1fc999 --- /dev/null +++ b/helix-core/src/diff.rs @@ -0,0 +1,70 @@ +use ropey::Rope; + +use crate::{Change, Transaction}; + +/// Compares `old` and `new` to generate a [`Transaction`] describing +/// the steps required to get from `old` to `new`. +pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction { + // `similar` only works on contiguous data, so a `Rope` has + // to be temporarily converted into a `String`. + let old_converted = old.to_string(); + let new_converted = new.to_string(); + + // A timeout is set so after 1 seconds, the algorithm will start + // approximating. This is especially important for big `Rope`s or + // `Rope`s that are extremely dissimilar to each other. + // + // Note: Ignore the clippy warning, as the trait bounds of + // `Transaction::change()` require an iterator implementing + // `ExactIterator`. + let mut config = similar::TextDiff::configure(); + config.timeout(std::time::Duration::from_secs(1)); + + let diff = config.diff_chars(&old_converted, &new_converted); + + // The current position of the change needs to be tracked to + // construct the `Change`s. + let mut pos = 0; + let changes: Vec = diff + .ops() + .iter() + .map(|op| op.as_tag_tuple()) + .filter_map(|(tag, old_range, new_range)| { + // `old_pos..pos` is equivalent to `start..end` for where + // the change should be applied. + let old_pos = pos; + pos += old_range.end - old_range.start; + + match tag { + // Semantically, inserts and replacements are the same thing. + similar::DiffTag::Insert | similar::DiffTag::Replace => { + // This is the text from the `new` rope that should be + // inserted into `old`. + let text: &str = { + let start = new.char_to_byte(new_range.start); + let end = new.char_to_byte(new_range.end); + &new_converted[start..end] + }; + Some((old_pos, pos, Some(text.into()))) + } + similar::DiffTag::Delete => Some((old_pos, pos, None)), + similar::DiffTag::Equal => None, + } + }) + .collect(); + Transaction::change(old, changes.into_iter()) +} + +#[cfg(test)] +mod tests { + use super::*; + + quickcheck::quickcheck! { + fn test_compare_ropes(a: String, b: String) -> bool { + let mut old = Rope::from(a); + let new = Rope::from(b); + compare_ropes(&old, &new).apply(&mut old); + old.to_string() == new.to_string() + } + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index dfbbd748..c2bb8c55 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -2,6 +2,7 @@ pub mod auto_pairs; pub mod chars; pub mod comment; pub mod diagnostic; +pub mod diff; pub mod graphemes; pub mod history; pub mod indent; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a3799e7e..860d8e22 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1521,6 +1521,24 @@ mod cmd { } } + /// Sets the [`Document`]'s encoding.. + fn set_encoding(cx: &mut compositor::Context, args: &[&str], _: PromptEvent) { + let (_, doc) = current!(cx.editor); + if let Some(label) = args.first() { + doc.set_encoding(label) + .unwrap_or_else(|e| cx.editor.set_error(e.to_string())); + } else { + let encoding = doc.encoding().name().to_string(); + cx.editor.set_status(encoding) + } + } + + /// Reload the [`Document`] from its source file. + fn reload(cx: &mut compositor::Context, _args: &[&str], _: PromptEvent) { + let (view, doc) = current!(cx.editor); + doc.reload(view.id).unwrap(); + } + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -1704,6 +1722,20 @@ mod cmd { fun: show_current_directory, completer: None, }, + TypableCommand { + name: "encoding", + alias: None, + doc: "Set encoding based on `https://encoding.spec.whatwg.org`", + fun: set_encoding, + completer: None, + }, + TypableCommand { + name: "reload", + alias: None, + doc: "Discard changes and reload from the source file.", + fun: reload, + completer: None, + } ]; pub static COMMANDS: Lazy> = Lazy::new(|| { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0f1f3a8f..f85ded11 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -307,6 +307,19 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( Ok(()) } +/// Inserts the final line ending into `rope` if it's missing. [Why?](https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline) +pub fn with_line_ending(rope: &mut Rope) -> LineEnding { + // search for line endings + let line_ending = auto_detect_line_ending(rope).unwrap_or(DEFAULT_LINE_ENDING); + + // add missing newline at the end of file + if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) { + rope.insert(rope.len_chars(), line_ending.as_str()); + } + + line_ending +} + /// Like std::mem::replace() except it allows the replacement value to be mapped from the /// original value. fn take_with(mut_ref: &mut T, closure: F) @@ -449,14 +462,7 @@ impl Document { let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?; let (mut rope, encoding) = from_reader(&mut file, encoding)?; - - // search for line endings - let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING); - - // add missing newline at the end of file - if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) { - rope.insert(rope.len_chars(), line_ending.as_str()); - } + let line_ending = with_line_ending(&mut rope); let mut doc = Self::from(rope, Some(encoding)); @@ -586,6 +592,45 @@ impl Document { } } + /// Reload the document from its path. + pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> { + let encoding = &self.encoding; + let path = self.path().filter(|path| path.exists()); + + // If there is no path or the path no longer exists. + if path.is_none() { + return Err(anyhow!("can't find file to reload from")); + } + + let mut file = std::fs::File::open(path.unwrap())?; + let (mut rope, ..) = from_reader(&mut file, Some(encoding))?; + let line_ending = with_line_ending(&mut rope); + + let transaction = helix_core::diff::compare_ropes(self.text(), &rope); + self.apply(&transaction, view_id); + self.append_changes_to_history(view_id); + + // Detect indentation style and set line ending. + self.detect_indent_style(); + self.line_ending = line_ending; + + Ok(()) + } + + /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`. + pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> { + match encoding_rs::Encoding::for_label(label.as_bytes()) { + Some(encoding) => self.encoding = encoding, + None => return Err(anyhow::anyhow!("unknown encoding")), + } + Ok(()) + } + + /// Returns the [`Document`]'s current encoding. + pub fn encoding(&self) -> &'static encoding_rs::Encoding { + self.encoding + } + fn detect_indent_style(&mut self) { // Build a histogram of the indentation *increases* between // subsequent lines, ignoring lines that are all whitespace. From c68fe1f2a3a40c37969c1f5d18e3134320a0c773 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 3 Jul 2021 06:37:49 +0530 Subject: [PATCH 03/28] Add object selection (textobjects) (#385) * Add textobjects for word * Add textobjects for surround characters * Apply clippy lints * Remove ThisWordPrevBound in favor of PrevWordEnd It's the same as PrevWordEnd except for taking the current char into account, so use a "flag" to capture that usecase * Add tests for PrevWordEnd movement * Remove ThisWord* movements They did not preserve anchor positions and were only used for textobject boundary search anyway so replace them with simple position finding functions * Rewrite tests of word textobject * Add tests for surround textobject * Add textobject docs * Refactor textobject word position functions * Apply clippy lints on textobject * Fix overflow error with textobjects --- book/src/keymap.md | 5 +- book/src/usage.md | 16 ++ helix-core/src/lib.rs | 1 + helix-core/src/movement.rs | 99 ++++++++++- helix-core/src/selection.rs | 10 ++ helix-core/src/textobject.rs | 319 +++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 32 +++- 7 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 helix-core/src/textobject.rs diff --git a/book/src/keymap.md b/book/src/keymap.md index 0265fe6d..6b7ccd11 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -150,7 +150,8 @@ Jumps to various locations. ## Match mode Enter this mode using `m` from normal mode. See the relavant section -in [Usage](./usage.md#surround) for an explanation about surround usage. +in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround) +and [textobject](./usage.md#textobject) usage. | Key | Description | | ----- | ----------- | @@ -158,6 +159,8 @@ in [Usage](./usage.md#surround) for an explanation about surround usage. | `s` `` | Surround current selection with `` | | `r` `` | Replace surround character `` with `` | | `d` `` | Delete surround character `` | +| `a` `` | Select around textobject | +| `i` `` | Select inside textobject | ## Object mode diff --git a/book/src/usage.md b/book/src/usage.md index e6bd60e2..0458071a 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -24,3 +24,19 @@ It can also act on multiple seletions (yay!). For example, to change every occur - `mr([` to replace the parens with square brackets Multiple characters are currently not supported, but planned. + +## Textobjects + +Currently supported: `word`, `surround`. + +![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif) + +- `ma` - Select around the object (`va` in vim, `` in kakoune) +- `mi` - Select inside the object (`vi` in vim, `` in kakoune) + +| Key after `mi` or `ma` | Textobject selected | +| --- | --- | +| `w` | Word | +| `(`, `[`, `'`, etc | Specified surround pairs | + +Textobjects based on treesitter, like `function`, `class`, etc are planned. diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index c2bb8c55..3684a93e 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -18,6 +18,7 @@ pub mod selection; mod state; pub mod surround; pub mod syntax; +pub mod textobject; mod transaction; pub mod unicode { diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index acc95e7e..f9e5deb4 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -113,6 +113,10 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) - word_move(slice, range, count, WordMotionTarget::PrevLongWordStart) } +pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { + word_move(slice, range, count, WordMotionTarget::PrevWordEnd) +} + fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { (0..count).fold(range, |range, _| { slice.chars_at(range.head).range_to_target(target, range) @@ -159,6 +163,7 @@ pub enum WordMotionTarget { NextWordStart, NextWordEnd, PrevWordStart, + PrevWordEnd, // A "Long word" (also known as a WORD in vim/kakoune) is strictly // delimited by whitespace, and can consist of punctuation as well // as alphanumerics. @@ -181,7 +186,9 @@ impl CharHelpers for Chars<'_> { fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range { // Characters are iterated forward or backwards depending on the motion direction. let characters: Box> = match target { - WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => { + WordMotionTarget::PrevWordStart + | WordMotionTarget::PrevLongWordStart + | WordMotionTarget::PrevWordEnd => { self.next(); Box::new(from_fn(|| self.prev())) } @@ -190,9 +197,9 @@ impl CharHelpers for Chars<'_> { // Index advancement also depends on the direction. let advance: &dyn Fn(&mut usize) = match target { - WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => { - &|u| *u = u.saturating_sub(1) - } + WordMotionTarget::PrevWordStart + | WordMotionTarget::PrevLongWordStart + | WordMotionTarget::PrevWordEnd => &|u| *u = u.saturating_sub(1), _ => &|u| *u += 1, }; @@ -265,7 +272,7 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char> }; match target { - WordMotionTarget::NextWordStart => { + WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => { is_word_boundary(peek, *next_peek) && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()) } @@ -913,6 +920,88 @@ mod test { } } + #[test] + fn test_behaviour_when_moving_to_end_of_previous_words() { + let tests = array::IntoIter::new([ + ("Basic backward motion from the middle of a word", + vec![(1, Range::new(9, 9), Range::new(9, 5))]), + ("Starting from after boundary retreats the anchor", + vec![(1, Range::new(0, 13), Range::new(12, 8))]), + ("Jump to end of a word succeeded by whitespace", + vec![(1, Range::new(10, 10), Range::new(10, 4))]), + (" Jump to start of line from end of word preceded by whitespace", + vec![(1, Range::new(7, 7), Range::new(7, 0))]), + ("Previous anchor is irrelevant for backward motions", + vec![(1, Range::new(26, 12), Range::new(12, 8))]), + (" Starting from whitespace moves to first space in sequence", + vec![(1, Range::new(0, 3), Range::new(3, 0))]), + ("Test identifiers_with_underscores are considered a single word", + vec![(1, Range::new(0, 25), Range::new(25, 4))]), + ("Jumping\n \nback through a newline selects whitespace", + vec![(1, Range::new(0, 13), Range::new(11, 8))]), + ("Jumping to start of word from the end selects the whole word", + vec![(1, Range::new(15, 15), Range::new(15, 10))]), + ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion", + vec![ + (1, Range::new(30, 30), Range::new(30, 21)), + (1, Range::new(30, 21), Range::new(20, 18)), + (1, Range::new(20, 18), Range::new(17, 15)) + ]), + + ("... ... punctuation and spaces behave as expected", + vec![ + (1, Range::new(0, 10), Range::new(9, 9)), + (1, Range::new(9, 6), Range::new(5, 3)), + ]), + (".._.._ punctuation is not joined by underscores into a single block", + vec![(1, Range::new(0, 5), Range::new(4, 3))]), + ("Newlines\n\nare bridged seamlessly.", + vec![ + (1, Range::new(0, 10), Range::new(7, 0)), + ]), + ("Jumping \n\n\n\n\nback from within a newline group selects previous block", + vec![ + (1, Range::new(0, 13), Range::new(10, 7)), + ]), + ("Failed motions do not modify the range", + vec![ + (0, Range::new(3, 0), Range::new(3, 0)), + ]), + ("Multiple motions at once resolve correctly", + vec![ + (3, Range::new(23, 23), Range::new(15, 8)), + ]), + ("Excessive motions are performed partially", + vec![ + (999, Range::new(40, 40), Range::new(8, 0)), + ]), + ("", // Edge case of moving backwards in empty string + vec![ + (1, Range::new(0, 0), Range::new(0, 0)), + ]), + ("\n\n\n\n\n", // Edge case of moving backwards in all newlines + vec![ + (1, Range::new(0, 0), Range::new(0, 0)), + ]), + (" \n \nJumping back through alternated space blocks and newlines selects the space blocks", + vec![ + (1, Range::new(0, 7), Range::new(6, 4)), + (1, Range::new(6, 4), Range::new(2, 0)), + ]), + ("Test ヒーリクス multibyte characters behave as normal characters", + vec![ + (1, Range::new(0, 9), Range::new(9, 4)), + ]), + ]); + + for (sample, scenario) in tests { + for (count, begin, expected_end) in scenario.into_iter() { + let range = move_prev_word_end(Rope::from(sample).slice(..), begin, count); + assert_eq!(range, expected_end, "Case failed: [{}]", sample); + } + } + } + #[test] fn test_behaviour_when_moving_to_end_of_next_long_words() { let tests = array::IntoIter::new([ diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index d99e2aff..63b9b557 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -130,6 +130,16 @@ impl Range { } } +impl From<(usize, usize)> for Range { + fn from(tuple: (usize, usize)) -> Self { + Self { + anchor: tuple.0, + head: tuple.1, + horiz: None, + } + } +} + /// A selection consists of one or more selection ranges. /// invariant: A selection can never be empty (always contains at least primary range). #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs new file mode 100644 index 00000000..d29eb03c --- /dev/null +++ b/helix-core/src/textobject.rs @@ -0,0 +1,319 @@ +use ropey::RopeSlice; + +use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory}; +use crate::movement::{self, Direction}; +use crate::surround; +use crate::Range; + +fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize { + this_word_bound_pos(slice, pos, Direction::Forward) +} + +fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize { + this_word_bound_pos(slice, pos, Direction::Backward) +} + +fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { + let iter = match direction { + Direction::Forward => slice.chars_at(pos + 1), + Direction::Backward => { + let mut iter = slice.chars_at(pos); + iter.reverse(); + iter + } + }; + + match categorize_char(slice.char(pos)) { + CharCategory::Eol | CharCategory::Whitespace => pos, + category => { + for peek in iter { + let curr_category = categorize_char(peek); + if curr_category != category + || curr_category == CharCategory::Eol + || curr_category == CharCategory::Whitespace + { + return pos; + } + pos = match direction { + Direction::Forward => pos + 1, + Direction::Backward => pos.saturating_sub(1), + } + } + pos + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum TextObject { + Around, + Inside, +} + +// count doesn't do anything yet +pub fn textobject_word( + slice: RopeSlice, + range: Range, + textobject: TextObject, + count: usize, +) -> Range { + let this_word_start = this_word_start_pos(slice, range.head); + let this_word_end = this_word_end_pos(slice, range.head); + + let (anchor, head); + match textobject { + TextObject::Inside => { + anchor = this_word_start; + head = this_word_end; + } + TextObject::Around => { + if slice + .get_char(this_word_end + 1) + .map_or(true, char_is_line_ending) + { + head = this_word_end; + if slice + .get_char(this_word_start.saturating_sub(1)) + .map_or(true, char_is_line_ending) + { + // single word on a line + anchor = this_word_start; + } else { + // last word on a line, select the whitespace before it too + anchor = movement::move_prev_word_end(slice, range, count).head; + } + } else if char_is_whitespace(slice.char(range.head)) { + // select whole whitespace and next word + head = movement::move_next_word_end(slice, range, count).head; + anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace()) + .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos + .unwrap_or(0); + } else { + head = movement::move_next_word_start(slice, range, count).head; + anchor = this_word_start; + } + } + }; + Range::new(anchor, head) +} + +pub fn textobject_surround( + slice: RopeSlice, + range: Range, + textobject: TextObject, + ch: char, + count: usize, +) -> Range { + surround::find_nth_pairs_pos(slice, ch, range.head, count) + .map(|(anchor, head)| match textobject { + TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)), + TextObject::Around => Range::new(anchor, head), + }) + .unwrap_or(range) +} + +#[cfg(test)] +mod test { + use super::TextObject::*; + use super::*; + + use crate::Range; + use ropey::Rope; + + #[test] + fn test_textobject_word() { + // (text, [(cursor position, textobject, final range), ...]) + let tests = &[ + ( + "cursor at beginning of doc", + vec![(0, Inside, (0, 5)), (0, Around, (0, 6))], + ), + ( + "cursor at middle of word", + vec![ + (13, Inside, (10, 15)), + (10, Inside, (10, 15)), + (15, Inside, (10, 15)), + (13, Around, (10, 16)), + (10, Around, (10, 16)), + (15, Around, (10, 16)), + ], + ), + ( + "cursor between word whitespace", + vec![(6, Inside, (6, 6)), (6, Around, (6, 13))], + ), + ( + "cursor on word before newline\n", + vec![ + (22, Inside, (22, 28)), + (28, Inside, (22, 28)), + (25, Inside, (22, 28)), + (22, Around, (21, 28)), + (28, Around, (21, 28)), + (25, Around, (21, 28)), + ], + ), + ( + "cursor on newline\nnext line", + vec![(17, Inside, (17, 17)), (17, Around, (17, 22))], + ), + ( + "cursor on word after newline\nnext line", + vec![ + (29, Inside, (29, 32)), + (30, Inside, (29, 32)), + (32, Inside, (29, 32)), + (29, Around, (29, 33)), + (30, Around, (29, 33)), + (32, Around, (29, 33)), + ], + ), + ( + "cursor on #$%:;* punctuation", + vec![ + (13, Inside, (10, 15)), + (10, Inside, (10, 15)), + (15, Inside, (10, 15)), + (13, Around, (10, 16)), + (10, Around, (10, 16)), + (15, Around, (10, 16)), + ], + ), + ( + "cursor on punc%^#$:;.tuation", + vec![ + (14, Inside, (14, 20)), + (20, Inside, (14, 20)), + (17, Inside, (14, 20)), + (14, Around, (14, 20)), + // FIXME: edge case + // (20, Around, (14, 20)), + (17, Around, (14, 20)), + ], + ), + ( + "cursor in extra whitespace", + vec![ + (9, Inside, (9, 9)), + (10, Inside, (10, 10)), + (11, Inside, (11, 11)), + (9, Around, (9, 16)), + (10, Around, (9, 16)), + (11, Around, (9, 16)), + ], + ), + ( + "cursor at end of doc", + vec![(19, Inside, (17, 19)), (19, Around, (16, 19))], + ), + ]; + + for (sample, scenario) in tests { + let doc = Rope::from(*sample); + let slice = doc.slice(..); + for &case in scenario { + let (pos, objtype, expected_range) = case; + let result = textobject_word(slice, Range::point(pos), objtype, 1); + assert_eq!( + result, + expected_range.into(), + "\nCase failed: {:?} - {:?}", + sample, + case + ); + } + } + } + + #[test] + fn test_textobject_surround() { + // (text, [(cursor position, textobject, final range, count), ...]) + let tests = &[ + ( + "simple (single) surround pairs", + vec![ + (3, Inside, (3, 3), '(', 1), + (7, Inside, (8, 13), ')', 1), + (10, Inside, (8, 13), '(', 1), + (14, Inside, (8, 13), ')', 1), + (3, Around, (3, 3), '(', 1), + (7, Around, (7, 14), ')', 1), + (10, Around, (7, 14), '(', 1), + (14, Around, (7, 14), ')', 1), + ], + ), + ( + "samexx 'single' surround pairs", + vec![ + (3, Inside, (3, 3), '\'', 1), + // FIXME: surround doesn't work when *on* same chars pair + // (7, Inner, (8, 13), '\'', 1), + (10, Inside, (8, 13), '\'', 1), + // (14, Inner, (8, 13), '\'', 1), + (3, Around, (3, 3), '\'', 1), + // (7, Around, (7, 14), '\'', 1), + (10, Around, (7, 14), '\'', 1), + // (14, Around, (7, 14), '\'', 1), + ], + ), + ( + "(nested (surround (pairs)) 3 levels)", + vec![ + (0, Inside, (1, 34), '(', 1), + (6, Inside, (1, 34), ')', 1), + (8, Inside, (9, 24), '(', 1), + (8, Inside, (9, 34), ')', 2), + (20, Inside, (9, 24), '(', 2), + (20, Inside, (1, 34), ')', 3), + (0, Around, (0, 35), '(', 1), + (6, Around, (0, 35), ')', 1), + (8, Around, (8, 25), '(', 1), + (8, Around, (8, 35), ')', 2), + (20, Around, (8, 25), '(', 2), + (20, Around, (0, 35), ')', 3), + ], + ), + ( + "(mixed {surround [pair] same} line)", + vec![ + (2, Inside, (1, 33), '(', 1), + (9, Inside, (8, 27), '{', 1), + (18, Inside, (18, 21), '[', 1), + (2, Around, (0, 34), '(', 1), + (9, Around, (7, 28), '{', 1), + (18, Around, (17, 22), '[', 1), + ], + ), + ( + "(stepped (surround) pairs (should) skip)", + vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)], + ), + ( + "[surround pairs{\non different]\nlines}", + vec![ + (7, Inside, (1, 28), '[', 1), + (15, Inside, (16, 35), '{', 1), + (7, Around, (0, 29), '[', 1), + (15, Around, (15, 36), '{', 1), + ], + ), + ]; + + for (sample, scenario) in tests { + let doc = Rope::from(*sample); + let slice = doc.slice(..); + for &case in scenario { + let (pos, objtype, expected_range, ch, count) = case; + let result = textobject_surround(slice, Range::point(pos), objtype, ch, count); + assert_eq!( + result, + expected_range.into(), + "\nCase failed: {:?} - {:?}", + sample, + case + ); + } + } + } +} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 860d8e22..d198b803 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3502,6 +3502,9 @@ fn right_bracket_mode(cx: &mut Context) { }) } +use helix_core::surround; +use helix_core::textobject; + fn match_mode(cx: &mut Context) { let count = cx.count; cx.on_next_key(move |cx, event| { @@ -3517,13 +3520,40 @@ fn match_mode(cx: &mut Context) { 's' => surround_add(cx), 'r' => surround_replace(cx), 'd' => surround_delete(cx), + 'a' => select_textobject(cx, textobject::TextObject::Around), + 'i' => select_textobject(cx, textobject::TextObject::Inside), _ => (), } } }) } -use helix_core::surround; +fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { + let count = cx.count(); + cx.on_next_key(move |cx, event| { + if let KeyEvent { + code: KeyCode::Char(ch), + .. + } = event + { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).transform(|range| { + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => { + textobject::textobject_surround(text, range, objtype, ch, count) + } + _ => range, + } + }); + + doc.set_selection(view.id, selection); + } + }) +} fn surround_add(cx: &mut Context) { cx.on_next_key(move |cx, event| { From 83e7dd860209ac3f0ccfa40ba98f7c314fa2b186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sat, 3 Jul 2021 12:30:13 +0900 Subject: [PATCH 04/28] fix: Temporary fix for #402 --- helix-term/src/application.rs | 6 +++++- helix-term/src/job.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 9622ad91..17ba2652 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -160,7 +160,11 @@ impl Application { } self.render(); } - Some(callback) = self.jobs.next_job() => { + Some(callback) = self.jobs.futures.next() => { + self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); + self.render(); + } + Some(callback) = self.jobs.wait_futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index c2873513..2ac41926 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -16,9 +16,9 @@ pub struct Job { #[derive(Default)] pub struct Jobs { - futures: FuturesUnordered, + pub futures: FuturesUnordered, /// These are the ones that need to complete before we exit. - wait_futures: FuturesUnordered, + pub wait_futures: FuturesUnordered, } impl Job { @@ -77,11 +77,11 @@ impl Jobs { } } - pub fn next_job( - &mut self, - ) -> impl Future>>> + '_ { - future::select(self.futures.next(), self.wait_futures.next()) - .map(|either| either.factor_first().0) + pub async fn next_job(&mut self) -> Option>> { + tokio::select! { + event = self.futures.next() => { event } + event = self.wait_futures.next() => { event } + } } pub fn add(&mut self, j: Job) { From f909526ebd5e3371120c3f332bb8ad9174c4892b Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 3 Jul 2021 10:31:46 +0530 Subject: [PATCH 05/28] Update onedark theme Add colors for matching brace, non primary selections, inactive statusline --- runtime/themes/onedark.toml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index f478b05f..508109dd 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -29,16 +29,26 @@ "warning" = { fg = "#e5c07b", modifiers = ['bold'] } "error" = { fg = "#e06c75", modifiers = ['bold'] } -"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" } "ui.background" = { fg = "#ABB2BF", bg = "#282C34" } -"ui.help" = { bg = "#3E4452" } + +"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] } +"ui.cursor.match" = { fg = "#61AFEF", modifiers = ['underlined']} + +"ui.selection" = { bg = "#5C6370" } +"ui.selection.primary" = { bg = "#3E4452" } + "ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] } "ui.linenr.selected" = { fg = "#ABB2BF" } -"ui.popup" = { bg = "#3E4452" } + "ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" } -"ui.statusline.inactive" = { fg = "#ABB2Bf", bg = "#2C323C" } -"ui.selection" = { bg = "#3E4452" } +"ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" } + "ui.text" = { fg = "#ABB2BF", bg = "#282C34" } "ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] } + +"ui.help" = { bg = "#3E4452" } +"ui.popup" = { bg = "#3E4452" } "ui.window" = { bg = "#3E4452" } -# "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported +"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" } + From 37f0b9ee159c13a8bc225b28219dfc485f4393a2 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 3 Jul 2021 10:35:09 +0530 Subject: [PATCH 06/28] Add missing linenr.selected key to docs --- book/src/themes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/book/src/themes.md b/book/src/themes.md index d6ed78ba..e5c461fd 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -81,6 +81,7 @@ Possible keys: | `ui.cursor.match` | Matching bracket etc. | | `ui.cursor.primary` | Cursor with primary selection | | `ui.linenr` | | +| `ui.linenr.selected` | | | `ui.statusline` | | | `ui.statusline.inactive` | | | `ui.popup` | | From 351c1e7e5533a05a8e4da20d1e5227c356098b7b Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 3 Jul 2021 15:46:56 +0530 Subject: [PATCH 07/28] Fix surround bug when cursor on same pair For example when the cursor is _on_ the `'` in `'word'`, the cursor wouldn't move because the search for a matching pair started _from_ the position of the cursor and simply found itself. --- helix-core/src/surround.rs | 18 +++++++++++++----- helix-core/src/textobject.rs | 9 ++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 61981d6e..841288c2 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -41,11 +41,14 @@ pub fn find_nth_pairs_pos( let (open, close) = get_pair(ch); let (open_pos, close_pos) = if open == close { - // find_nth* do not consider current character; +1/-1 to include them - ( - search::find_nth_prev(text, open, pos + 1, n, true)?, - search::find_nth_next(text, close, pos - 1, n, true)?, - ) + let prev = search::find_nth_prev(text, open, pos, n, true); + let next = search::find_nth_next(text, close, pos, n, true); + if text.char(pos) == open { + // curosr is *on* a pair + next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))? + } else { + (prev?, next?) + } } else { ( find_nth_open_pair(text, open, close, pos, n)?, @@ -198,6 +201,11 @@ mod test { assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15))); assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21))); assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27))); + // cursor on the quotes + assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), Some((10, 15))); + // this is the best we can do since opening and closing pairs are same + assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4))); + assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27))); } #[test] diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index d29eb03c..fbf66256 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -247,14 +247,13 @@ mod test { "samexx 'single' surround pairs", vec![ (3, Inside, (3, 3), '\'', 1), - // FIXME: surround doesn't work when *on* same chars pair - // (7, Inner, (8, 13), '\'', 1), + (7, Inside, (8, 13), '\'', 1), (10, Inside, (8, 13), '\'', 1), - // (14, Inner, (8, 13), '\'', 1), + (14, Inside, (8, 13), '\'', 1), (3, Around, (3, 3), '\'', 1), - // (7, Around, (7, 14), '\'', 1), + (7, Around, (7, 14), '\'', 1), (10, Around, (7, 14), '\'', 1), - // (14, Around, (7, 14), '\'', 1), + (14, Around, (7, 14), '\'', 1), ], ), ( From 6ccfa229ed61552e243519534c8659c0974ef62e Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sat, 3 Jul 2021 19:58:18 +0800 Subject: [PATCH 08/28] Fix typo on comment in surround --- helix-core/src/surround.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 841288c2..52f60cab 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -44,7 +44,7 @@ pub fn find_nth_pairs_pos( let prev = search::find_nth_prev(text, open, pos, n, true); let next = search::find_nth_next(text, close, pos, n, true); if text.char(pos) == open { - // curosr is *on* a pair + // cursor is *on* a pair next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))? } else { (prev?, next?) From 8985c58fd328cde512df0ac7bc577bd8a8c949a0 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sat, 19 Jun 2021 23:54:37 +0800 Subject: [PATCH 09/28] Add infobox --- Cargo.lock | 1 + helix-term/src/commands.rs | 105 +++++++++++++++++------ helix-term/src/keymap.rs | 16 ++-- helix-term/src/ui/editor.rs | 7 +- helix-term/src/ui/info.rs | 24 ++++++ helix-term/src/ui/mod.rs | 1 + helix-view/Cargo.toml | 1 + helix-view/src/editor.rs | 3 + helix-view/src/info.rs | 51 +++++++++++ helix-view/src/input.rs | 164 ++++++++++++++++++++++++++---------- helix-view/src/lib.rs | 1 + 11 files changed, 296 insertions(+), 78 deletions(-) create mode 100644 helix-term/src/ui/info.rs create mode 100644 helix-view/src/info.rs diff --git a/Cargo.lock b/Cargo.lock index a377e2f4..59eb894e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,6 +419,7 @@ dependencies = [ "slotmap", "tokio", "toml", + "unicode-width", "url", "which", ] diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d198b803..106c58d1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -16,6 +16,7 @@ use helix_core::{ use helix_view::{ document::{IndentStyle, Mode}, editor::Action, + info::Info, input::KeyEvent, keyboard::KeyCode, view::{View, PADDING}, @@ -33,6 +34,7 @@ use movement::Movement; use crate::{ compositor::{self, Component, Compositor}, + key, ui::{self, Picker, Popup, Prompt, PromptEvent}, }; @@ -3400,33 +3402,88 @@ fn select_register(cx: &mut Context) { }) } -fn space_mode(cx: &mut Context) { - cx.on_next_key(move |cx, event| { - if let KeyEvent { - code: KeyCode::Char(ch), - .. - } = event - { - // TODO: temporarily show SPC in the mode list - match ch { - 'f' => file_picker(cx), - 'b' => buffer_picker(cx), - 's' => symbol_picker(cx), - 'w' => window_mode(cx), - 'y' => yank_joined_to_clipboard(cx), - 'Y' => yank_main_selection_to_clipboard(cx), - 'p' => paste_clipboard_after(cx), - 'P' => paste_clipboard_before(cx), - 'R' => replace_selections_with_clipboard(cx), - // ' ' => toggle_alternate_buffer(cx), - // TODO: temporary since space mode took its old key - ' ' => keep_primary_selection(cx), - _ => (), - } +macro_rules! mode_info { + // TODO: how to use one expr for both pat and expr? + // TODO: how to use replaced function name as str at compile time? + // TODO: extend to support multiple keys, but first solve the other two + {$name:literal, $cx:expr, $($key:expr => $func:expr; $funcs:literal),+,} => { + mode_info! { + $name, $cx, + $($key; $key => $func; $funcs,)+ } - }) + }; + {$name:literal, $cx:expr, $($key:expr; $keyp:pat => $func:expr; $funcs:literal),+,} => { + $cx.editor.autoinfo = Some(Info::key( + $name, + vec![ + $( + (vec![$key], $funcs), + )+ + ], + )); + $cx.on_next_key(move |cx, event| { + match event { + $( + $keyp => $func(cx), + )+ + _ => {} + } + }) + } } +fn space_mode(cx: &mut Context) { + mode_info! { + "space mode", cx, + key!('f'); key!('f') => file_picker; "file picker", + key!('b'); key!('b') => buffer_picker; "buffer picker", + key!('s'); key!('s') => symbol_picker; "symbol picker", + key!('w'); key!('w') => window_mode; "window mode", + key!('y'); key!('y') => yank_joined_to_clipboard; "yank joined to clipboard", + key!('Y'); key!('Y') => yank_main_selection_to_clipboard; "yank main selection to clipboard", + key!('p'); key!('p') => paste_clipboard_after; "paste clipboard after", + key!('P'); key!('P') => paste_clipboard_before; "paste clipboard before", + key!('R'); key!('R') => replace_selections_with_clipboard; "replace selections with clipboard", + key!(' '); key!(' ') => keep_primary_selection; "keep primary selection", + } +} + +// TODO: generated, delete it later +// fn space_mode(cx: &mut Context) { +// cx.editor.autoinfo = Some(Info::key( +// "space", +// vec![ +// (vec![key!('f')], "file picker"), +// (vec![key!('b')], "buffer picker"), +// (vec![key!('s')], "symbol picker"), +// (vec![key!('w')], "window mode"), +// (vec![key!('y')], "yank joined to clipboard"), +// (vec![key!('Y')], "yank main selection to clipboard"), +// (vec![key!('p')], "paste clipboard after"), +// (vec![key!('P')], "paste clipboard before"), +// (vec![key!('R')], "replace selections with clipboard"), +// (vec![key!(' ')], "keep primary selection"), +// ], +// )); +// cx.on_next_key(move |cx, event| { +// match event { +// key!('f') => file_picker(cx), +// key!('b') => buffer_picker(cx), +// key!('s') => symbol_picker(cx), +// key!('w') => window_mode(cx), +// key!('y') => yank_joined_to_clipboard(cx), +// key!('Y') => yank_main_selection_to_clipboard(cx), +// key!('p') => paste_clipboard_after(cx), +// key!('P') => paste_clipboard_before(cx), +// key!('R') => replace_selections_with_clipboard(cx), +// // key!(' ') => toggle_alternate_buffer(cx), +// // TODO: temporary since space mode took its old key +// key!(' ') => keep_primary_selection(cx), +// _ => {} +// } +// }) +// } + fn view_mode(cx: &mut Context) { cx.on_next_key(move |cx, event| { if let KeyEvent { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 53588a2b..53143c71 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -105,14 +105,14 @@ use std::{ macro_rules! key { ($key:ident) => { KeyEvent { - code: KeyCode::$key, - modifiers: KeyModifiers::NONE, + code: helix_view::keyboard::KeyCode::$key, + modifiers: helix_view::keyboard::KeyModifiers::NONE, } }; ($($ch:tt)*) => { KeyEvent { - code: KeyCode::Char($($ch)*), - modifiers: KeyModifiers::NONE, + code: helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: helix_view::keyboard::KeyModifiers::NONE, } }; } @@ -120,8 +120,8 @@ macro_rules! key { macro_rules! ctrl { ($($ch:tt)*) => { KeyEvent { - code: KeyCode::Char($($ch)*), - modifiers: KeyModifiers::CONTROL, + code: helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: helix_view::keyboard::KeyModifiers::CONTROL, } }; } @@ -129,8 +129,8 @@ macro_rules! ctrl { macro_rules! alt { ($($ch:tt)*) => { KeyEvent { - code: KeyCode::Char($($ch)*), - modifiers: KeyModifiers::ALT, + code: helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: helix_view::keyboard::KeyModifiers::ALT, } }; } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 14c34493..8b7c92de 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -717,6 +717,10 @@ impl Component for EditorView { self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused); } + if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) { + info.render(area, surface, cx); + } + // render status msg if let Some((status_msg, severity)) = &cx.editor.status_msg { use helix_view::editor::Severity; @@ -735,8 +739,7 @@ impl Component for EditorView { } if let Some(completion) = &self.completion { - completion.render(area, surface, cx) - // render completion here + completion.render(area, surface, cx); } } diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs new file mode 100644 index 00000000..085a2d9b --- /dev/null +++ b/helix-term/src/ui/info.rs @@ -0,0 +1,24 @@ +use crate::compositor::{Component, Context}; +use helix_view::graphics::{Margin, Rect, Style}; +use helix_view::info::Info; +use tui::buffer::Buffer as Surface; +use tui::widgets::{Block, Borders, Widget}; + +impl Component for Info { + fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { + let block = Block::default().title(self.title).borders(Borders::ALL); + let Info { width, height, .. } = self; + let (w, h) = (*width + 2, *height + 2); + // -2 to subtract command line + statusline. a bit of a hack, because of splits. + let area = Rect::new(viewport.width - w, viewport.height - h - 2, w, h); + let margin = Margin { + vertical: 1, + horizontal: 1, + }; + let Rect { x, y, .. } = area.inner(&margin); + for (y, line) in (y..).zip(self.text.lines()) { + surface.set_string(x, y, line, Style::default()); + } + block.render(area, surface); + } +} diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 7111c968..288d3d2e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,5 +1,6 @@ mod completion; mod editor; +mod info; mod markdown; mod menu; mod picker; diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index cb2032de..b6816d71 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -31,6 +31,7 @@ slotmap = "1" encoding_rs = "0.8" chardetng = "0.1" +unicode-width = "0.1" serde = { version = "1.0", features = ["derive"] } toml = "0.5" diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index a16cc50f..b006a124 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,6 +1,7 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, graphics::{CursorKind, Rect}, + info::Info, theme::{self, Theme}, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId, @@ -32,6 +33,7 @@ pub struct Editor { pub syn_loader: Arc, pub theme_loader: Arc, + pub autoinfo: Option, pub status_msg: Option<(String, Severity)>, } @@ -64,6 +66,7 @@ impl Editor { theme_loader: themes, registers: Registers::default(), clipboard_provider: get_clipboard_provider(), + autoinfo: None, status_msg: None, } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs new file mode 100644 index 00000000..eef8d3a1 --- /dev/null +++ b/helix-view/src/info.rs @@ -0,0 +1,51 @@ +use crate::input::KeyEvent; +use std::fmt::Write; +use unicode_width::UnicodeWidthStr; + +#[derive(Debug)] +/// Info box used in editor. Rendering logic will be in other crate. +pub struct Info { + /// Title kept as static str for now. + pub title: &'static str, + /// Text body, should contains newline. + pub text: String, + /// Body width. + pub width: u16, + /// Body height. + pub height: u16, +} + +impl Info { + pub fn key(title: &'static str, body: Vec<(Vec, &'static str)>) -> Info { + let keymaps_width: u16 = body + .iter() + .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::() - 2) + .max() + .unwrap(); + let mut text = String::new(); + let mut width = 0; + let height = body.len() as u16; + for (mut keyevents, desc) in body { + let keyevent = keyevents.remove(0); + let mut left = keymaps_width - keyevent.width() as u16; + write!(text, "{}", keyevent).ok(); + for keyevent in keyevents { + write!(text, ", {}", keyevent).ok(); + left -= 2 + keyevent.width() as u16; + } + for _ in 0..left { + text.push(' '); + } + if keymaps_width + 2 + (desc.width() as u16) > width { + width = keymaps_width + 2 + desc.width() as u16; + } + writeln!(text, " {}", &desc).ok(); + } + Info { + title, + text, + width, + height, + } + } +} diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 5f61ce14..6e8292e9 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -13,6 +13,32 @@ pub struct KeyEvent { pub modifiers: KeyModifiers, } +pub(crate) mod keys { + pub(crate) const BACKSPACE: &str = "backspace"; + pub(crate) const ENTER: &str = "ret"; + pub(crate) const LEFT: &str = "left"; + pub(crate) const RIGHT: &str = "right"; + pub(crate) const UP: &str = "up"; + pub(crate) const DOWN: &str = "down"; + pub(crate) const HOME: &str = "home"; + pub(crate) const END: &str = "end"; + pub(crate) const PAGEUP: &str = "pageup"; + pub(crate) const PAGEDOWN: &str = "pagedown"; + pub(crate) const TAB: &str = "tab"; + pub(crate) const BACKTAB: &str = "backtab"; + pub(crate) const DELETE: &str = "del"; + pub(crate) const INSERT: &str = "ins"; + pub(crate) const NULL: &str = "null"; + pub(crate) const ESC: &str = "esc"; + pub(crate) const SPACE: &str = "space"; + pub(crate) const LESS_THAN: &str = "lt"; + pub(crate) const GREATER_THAN: &str = "gt"; + pub(crate) const PLUS: &str = "plus"; + pub(crate) const MINUS: &str = "minus"; + pub(crate) const SEMICOLON: &str = "semicolon"; + pub(crate) const PERCENT: &str = "percent"; +} + impl fmt::Display for KeyEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( @@ -34,28 +60,29 @@ impl fmt::Display for KeyEvent { }, ))?; match self.code { - KeyCode::Backspace => f.write_str("backspace")?, - KeyCode::Enter => f.write_str("ret")?, - KeyCode::Left => f.write_str("left")?, - KeyCode::Right => f.write_str("right")?, - KeyCode::Up => f.write_str("up")?, - KeyCode::Down => f.write_str("down")?, - KeyCode::Home => f.write_str("home")?, - KeyCode::End => f.write_str("end")?, - KeyCode::PageUp => f.write_str("pageup")?, - KeyCode::PageDown => f.write_str("pagedown")?, - KeyCode::Tab => f.write_str("tab")?, - KeyCode::BackTab => f.write_str("backtab")?, - KeyCode::Delete => f.write_str("del")?, - KeyCode::Insert => f.write_str("ins")?, - KeyCode::Null => f.write_str("null")?, - KeyCode::Esc => f.write_str("esc")?, - KeyCode::Char('<') => f.write_str("lt")?, - KeyCode::Char('>') => f.write_str("gt")?, - KeyCode::Char('+') => f.write_str("plus")?, - KeyCode::Char('-') => f.write_str("minus")?, - KeyCode::Char(';') => f.write_str("semicolon")?, - KeyCode::Char('%') => f.write_str("percent")?, + KeyCode::Backspace => f.write_str(keys::BACKSPACE)?, + KeyCode::Enter => f.write_str(keys::ENTER)?, + KeyCode::Left => f.write_str(keys::LEFT)?, + KeyCode::Right => f.write_str(keys::RIGHT)?, + KeyCode::Up => f.write_str(keys::UP)?, + KeyCode::Down => f.write_str(keys::DOWN)?, + KeyCode::Home => f.write_str(keys::HOME)?, + KeyCode::End => f.write_str(keys::END)?, + KeyCode::PageUp => f.write_str(keys::PAGEUP)?, + KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?, + KeyCode::Tab => f.write_str(keys::TAB)?, + KeyCode::BackTab => f.write_str(keys::BACKTAB)?, + KeyCode::Delete => f.write_str(keys::DELETE)?, + KeyCode::Insert => f.write_str(keys::INSERT)?, + KeyCode::Null => f.write_str(keys::NULL)?, + KeyCode::Esc => f.write_str(keys::ESC)?, + KeyCode::Char(' ') => f.write_str(keys::SPACE)?, + KeyCode::Char('<') => f.write_str(keys::LESS_THAN)?, + KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?, + KeyCode::Char('+') => f.write_str(keys::PLUS)?, + KeyCode::Char('-') => f.write_str(keys::MINUS)?, + KeyCode::Char(';') => f.write_str(keys::SEMICOLON)?, + KeyCode::Char('%') => f.write_str(keys::PERCENT)?, KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?, KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, }; @@ -63,34 +90,83 @@ impl fmt::Display for KeyEvent { } } +impl unicode_width::UnicodeWidthStr for KeyEvent { + fn width(&self) -> usize { + use unicode_width::UnicodeWidthChar; + let mut width = match self.code { + KeyCode::Backspace => keys::BACKSPACE.len(), + KeyCode::Enter => keys::ENTER.len(), + KeyCode::Left => keys::LEFT.len(), + KeyCode::Right => keys::RIGHT.len(), + KeyCode::Up => keys::UP.len(), + KeyCode::Down => keys::DOWN.len(), + KeyCode::Home => keys::HOME.len(), + KeyCode::End => keys::END.len(), + KeyCode::PageUp => keys::PAGEUP.len(), + KeyCode::PageDown => keys::PAGEDOWN.len(), + KeyCode::Tab => keys::TAB.len(), + KeyCode::BackTab => keys::BACKTAB.len(), + KeyCode::Delete => keys::DELETE.len(), + KeyCode::Insert => keys::INSERT.len(), + KeyCode::Null => keys::NULL.len(), + KeyCode::Esc => keys::ESC.len(), + KeyCode::Char(' ') => keys::SPACE.len(), + KeyCode::Char('<') => keys::LESS_THAN.len(), + KeyCode::Char('>') => keys::GREATER_THAN.len(), + KeyCode::Char('+') => keys::PLUS.len(), + KeyCode::Char('-') => keys::MINUS.len(), + KeyCode::Char(';') => keys::SEMICOLON.len(), + KeyCode::Char('%') => keys::PERCENT.len(), + KeyCode::F(1..=9) => 2, + KeyCode::F(_) => 3, + KeyCode::Char(c) => c.width().unwrap_or(0), + }; + if self.modifiers.contains(KeyModifiers::SHIFT) { + width += 2; + } + if self.modifiers.contains(KeyModifiers::ALT) { + width += 2; + } + if self.modifiers.contains(KeyModifiers::CONTROL) { + width += 2; + } + width + } + + fn width_cjk(&self) -> usize { + self.width() + } +} + impl std::str::FromStr for KeyEvent { type Err = Error; fn from_str(s: &str) -> Result { let mut tokens: Vec<_> = s.split('-').collect(); let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? { - "backspace" => KeyCode::Backspace, - "space" => KeyCode::Char(' '), - "ret" => KeyCode::Enter, - "lt" => KeyCode::Char('<'), - "gt" => KeyCode::Char('>'), - "plus" => KeyCode::Char('+'), - "minus" => KeyCode::Char('-'), - "semicolon" => KeyCode::Char(';'), - "percent" => KeyCode::Char('%'), - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "up" => KeyCode::Down, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "tab" => KeyCode::Tab, - "backtab" => KeyCode::BackTab, - "del" => KeyCode::Delete, - "ins" => KeyCode::Insert, - "null" => KeyCode::Null, - "esc" => KeyCode::Esc, + keys::BACKSPACE => KeyCode::Backspace, + keys::ENTER => KeyCode::Enter, + keys::LEFT => KeyCode::Left, + keys::RIGHT => KeyCode::Right, + keys::UP => KeyCode::Up, + keys::DOWN => KeyCode::Down, + keys::HOME => KeyCode::Home, + keys::END => KeyCode::End, + keys::PAGEUP => KeyCode::PageUp, + keys::PAGEDOWN => KeyCode::PageDown, + keys::TAB => KeyCode::Tab, + keys::BACKTAB => KeyCode::BackTab, + keys::DELETE => KeyCode::Delete, + keys::INSERT => KeyCode::Insert, + keys::NULL => KeyCode::Null, + keys::ESC => KeyCode::Esc, + keys::SPACE => KeyCode::Char(' '), + keys::LESS_THAN => KeyCode::Char('<'), + keys::GREATER_THAN => KeyCode::Char('>'), + keys::PLUS => KeyCode::Char('+'), + keys::MINUS => KeyCode::Char('-'), + keys::SEMICOLON => KeyCode::Char(';'), + keys::PERCENT => KeyCode::Char('%'), single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), function if function.len() > 1 && function.starts_with('F') => { let function: String = function.chars().skip(1).collect(); diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index caed2952..9bcc0b7d 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -5,6 +5,7 @@ pub mod clipboard; pub mod document; pub mod editor; pub mod graphics; +pub mod info; pub mod input; pub mod keyboard; pub mod register_selection; From 4c190ec9d9da49adedf2e14cd813725d979b42e8 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Tue, 29 Jun 2021 15:13:30 +0800 Subject: [PATCH 10/28] Suggestions for infobox changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Benoît Cortier --- helix-term/src/commands.rs | 8 ++++---- helix-term/src/keymap.rs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 106c58d1..ddeded2d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3406,18 +3406,18 @@ macro_rules! mode_info { // TODO: how to use one expr for both pat and expr? // TODO: how to use replaced function name as str at compile time? // TODO: extend to support multiple keys, but first solve the other two - {$name:literal, $cx:expr, $($key:expr => $func:expr; $funcs:literal),+,} => { + {$name:literal, $cx:expr, $($key:expr => $func:expr; $desc:literal),+,} => { mode_info! { $name, $cx, - $($key; $key => $func; $funcs,)+ + $($key; $key => $func; $desc,)+ } }; - {$name:literal, $cx:expr, $($key:expr; $keyp:pat => $func:expr; $funcs:literal),+,} => { + {$name:literal, $cx:expr, $($key:expr; $keyp:pat => $func:expr; $desc:literal),+,} => { $cx.editor.autoinfo = Some(Info::key( $name, vec![ $( - (vec![$key], $funcs), + (vec![$key], $desc), )+ ], )); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 53143c71..ef4a2138 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -105,14 +105,14 @@ use std::{ macro_rules! key { ($key:ident) => { KeyEvent { - code: helix_view::keyboard::KeyCode::$key, - modifiers: helix_view::keyboard::KeyModifiers::NONE, + code: ::helix_view::keyboard::KeyCode::$key, + modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; ($($ch:tt)*) => { KeyEvent { - code: helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: helix_view::keyboard::KeyModifiers::NONE, + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; } @@ -120,8 +120,8 @@ macro_rules! key { macro_rules! ctrl { ($($ch:tt)*) => { KeyEvent { - code: helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: helix_view::keyboard::KeyModifiers::CONTROL, + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, } }; } @@ -129,8 +129,8 @@ macro_rules! ctrl { macro_rules! alt { ($($ch:tt)*) => { KeyEvent { - code: helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: helix_view::keyboard::KeyModifiers::ALT, + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::ALT, } }; } From 9effe71b7d2133f18545d182cef384ea3fd1c0ff Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Wed, 30 Jun 2021 00:17:16 +0800 Subject: [PATCH 11/28] Apply suggestions from blaz for infobox --- Cargo.lock | 2 -- helix-term/src/ui/info.rs | 6 +----- helix-tui/Cargo.toml | 1 - helix-tui/src/backend/test.rs | 2 +- helix-tui/src/buffer.rs | 2 +- helix-tui/src/text.rs | 2 +- helix-tui/src/widgets/paragraph.rs | 2 +- helix-tui/src/widgets/reflow.rs | 2 +- helix-tui/src/widgets/table.rs | 2 +- helix-view/Cargo.toml | 1 - helix-view/src/info.rs | 2 +- helix-view/src/input.rs | 5 +++-- 12 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59eb894e..2cd202f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,7 +396,6 @@ dependencies = [ "helix-view", "serde", "unicode-segmentation", - "unicode-width", ] [[package]] @@ -419,7 +418,6 @@ dependencies = [ "slotmap", "tokio", "toml", - "unicode-width", "url", "which", ] diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index 085a2d9b..c5709356 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -11,11 +11,7 @@ impl Component for Info { let (w, h) = (*width + 2, *height + 2); // -2 to subtract command line + statusline. a bit of a hack, because of splits. let area = Rect::new(viewport.width - w, viewport.height - h - 2, w, h); - let margin = Margin { - vertical: 1, - horizontal: 1, - }; - let Rect { x, y, .. } = area.inner(&margin); + let Rect { x, y, .. } = block.inner(area); for (y, line) in (y..).zip(self.text.lines()) { surface.set_string(x, y, line, Style::default()); } diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index dde2eafe..7f98144c 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -19,7 +19,6 @@ default = ["crossterm"] bitflags = "1.0" cassowary = "0.3" unicode-segmentation = "1.2" -unicode-width = "0.1" crossterm = { version = "0.20", optional = true } serde = { version = "1", "optional" = true, features = ["derive"]} helix-view = { version = "0.3", path = "../helix-view", features = ["term"] } diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs index a03bcd8e..3f56b49c 100644 --- a/helix-tui/src/backend/test.rs +++ b/helix-tui/src/backend/test.rs @@ -2,9 +2,9 @@ use crate::{ backend::Backend, buffer::{Buffer, Cell}, }; +use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::{CursorKind, Rect}; use std::{fmt::Write, io}; -use unicode_width::UnicodeWidthStr; /// A backend used for the integration tests. #[derive(Debug)] diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 3a7ad144..377e3e39 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -1,7 +1,7 @@ use crate::text::{Span, Spans}; +use helix_core::unicode::width::UnicodeWidthStr; use std::cmp::min; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; use helix_view::graphics::{Color, Modifier, Rect, Style}; diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 4af6b09d..b8e52479 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -47,10 +47,10 @@ //! ]); //! ``` use helix_core::line_ending::str_is_line_ending; +use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::Style; use std::borrow::Cow; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; /// A grapheme associated to a style. #[derive(Debug, Clone, PartialEq)] diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs index bdfb5b9a..fee35d25 100644 --- a/helix-tui/src/widgets/paragraph.rs +++ b/helix-tui/src/widgets/paragraph.rs @@ -7,9 +7,9 @@ use crate::{ Block, Widget, }, }; +use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::{Rect, Style}; use std::iter; -use unicode_width::UnicodeWidthStr; fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { match alignment { diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs index ae561a4f..21847783 100644 --- a/helix-tui/src/widgets/reflow.rs +++ b/helix-tui/src/widgets/reflow.rs @@ -1,7 +1,7 @@ use crate::text::StyledGrapheme; use helix_core::line_ending::str_is_line_ending; +use helix_core::unicode::width::UnicodeWidthStr; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; const NBSP: &str = "\u{00a0}"; diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index ee5147b7..1ee4286a 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -9,9 +9,9 @@ use cassowary::{ WeightedRelation::*, {Expression, Solver}, }; +use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::{Rect, Style}; use std::collections::HashMap; -use unicode_width::UnicodeWidthStr; /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index b6816d71..cb2032de 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -31,7 +31,6 @@ slotmap = "1" encoding_rs = "0.8" chardetng = "0.1" -unicode-width = "0.1" serde = { version = "1.0", features = ["derive"] } toml = "0.5" diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index eef8d3a1..0eaab783 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,6 +1,6 @@ use crate::input::KeyEvent; +use helix_core::unicode::width::UnicodeWidthStr; use std::fmt::Write; -use unicode_width::UnicodeWidthStr; #[derive(Debug)] /// Info box used in editor. Rendering logic will be in other crate. diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 6e8292e9..2847bb69 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -1,5 +1,6 @@ //! Input event handling, currently backed by crossterm. use anyhow::{anyhow, Error}; +use helix_core::unicode::width::UnicodeWidthStr; use serde::de::{self, Deserialize, Deserializer}; use std::fmt; @@ -90,9 +91,9 @@ impl fmt::Display for KeyEvent { } } -impl unicode_width::UnicodeWidthStr for KeyEvent { +impl UnicodeWidthStr for KeyEvent { fn width(&self) -> usize { - use unicode_width::UnicodeWidthChar; + use helix_core::unicode::width::UnicodeWidthChar; let mut width = match self.code { KeyCode::Backspace => keys::BACKSPACE.len(), KeyCode::Enter => keys::ENTER.len(), From 6710855eac4826ffdc77d22901b010ae20bbaf3e Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Wed, 30 Jun 2021 21:39:21 +0800 Subject: [PATCH 12/28] Fix rendering issues for infobox --- helix-term/src/commands.rs | 6 ++++-- helix-term/src/ui/editor.rs | 1 + helix-term/src/ui/info.rs | 9 +++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ddeded2d..854c34d3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3422,6 +3422,7 @@ macro_rules! mode_info { ], )); $cx.on_next_key(move |cx, event| { + cx.editor.autoinfo = None; match event { $( $keyp => $func(cx), @@ -3441,8 +3442,8 @@ fn space_mode(cx: &mut Context) { key!('w'); key!('w') => window_mode; "window mode", key!('y'); key!('y') => yank_joined_to_clipboard; "yank joined to clipboard", key!('Y'); key!('Y') => yank_main_selection_to_clipboard; "yank main selection to clipboard", - key!('p'); key!('p') => paste_clipboard_after; "paste clipboard after", - key!('P'); key!('P') => paste_clipboard_before; "paste clipboard before", + key!('p'); key!('p') => paste_clipboard_after; "paste system clipboard after selections", + key!('P'); key!('P') => paste_clipboard_before; "paste system clipboard before selections", key!('R'); key!('R') => replace_selections_with_clipboard; "replace selections with clipboard", key!(' '); key!(' ') => keep_primary_selection; "keep primary selection", } @@ -3466,6 +3467,7 @@ fn space_mode(cx: &mut Context) { // ], // )); // cx.on_next_key(move |cx, event| { +// cx.editor.autoinfo = None; // match event { // key!('f') => file_picker(cx), // key!('b') => buffer_picker(cx), diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 8b7c92de..ad4f73bc 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -719,6 +719,7 @@ impl Component for EditorView { if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) { info.render(area, surface, cx); + cx.editor.autoinfo = Some(info); } // render status msg diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index c5709356..87c2c213 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -6,14 +6,19 @@ use tui::widgets::{Block, Borders, Widget}; impl Component for Info { fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - let block = Block::default().title(self.title).borders(Borders::ALL); + let style = cx.editor.theme.get("ui.popup"); + let block = Block::default() + .title(self.title) + .borders(Borders::ALL) + .border_style(style); let Info { width, height, .. } = self; let (w, h) = (*width + 2, *height + 2); // -2 to subtract command line + statusline. a bit of a hack, because of splits. let area = Rect::new(viewport.width - w, viewport.height - h - 2, w, h); + surface.clear_with(area, style); let Rect { x, y, .. } = block.inner(area); for (y, line) in (y..).zip(self.text.lines()) { - surface.set_string(x, y, line, Style::default()); + surface.set_string(x, y, line, style); } block.render(area, surface); } From 61e925cbed48ed4b6c629d611ba65e2f5ab33acc Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Thu, 1 Jul 2021 09:20:00 +0800 Subject: [PATCH 13/28] Add infobox doc generation and improve ergonomics --- helix-term/src/commands.rs | 156 +++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 67 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 854c34d3..9d3a5df9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -40,6 +40,7 @@ use crate::{ use crate::job::{self, Job, Jobs}; use futures_util::{FutureExt, TryFutureExt}; +use std::collections::HashMap; use std::{fmt, future::Future}; use std::{ @@ -76,6 +77,16 @@ impl<'a> Context<'a> { self.on_next_key_callback = Some(Box::new(on_next_key_callback)); } + #[inline] + pub fn on_next_key_mode(&mut self, map: HashMap) { + self.on_next_key(move |cx, event| { + cx.editor.autoinfo = None; + if let Some(func) = map.get(&event) { + func(cx); + } + }); + } + #[inline] pub fn callback( &mut self, @@ -3406,84 +3417,95 @@ macro_rules! mode_info { // TODO: how to use one expr for both pat and expr? // TODO: how to use replaced function name as str at compile time? // TODO: extend to support multiple keys, but first solve the other two - {$name:literal, $cx:expr, $($key:expr => $func:expr; $desc:literal),+,} => { - mode_info! { - $name, $cx, - $($key; $key => $func; $desc,)+ + (@join $first:expr $(,$rest:expr)*) => { + concat!($first, $(", ", $rest),*) + }; + {$mode:ident, $name:literal, $(#[doc = $desc:literal] $($key:expr),+ => $func:expr),+,} => { + #[doc = $name] + #[doc = ""] + #[doc = ""] + $( + #[doc = ""] + )+ + #[doc = "
keydesc
"] + #[doc = mode_info!(@join $($key),+)] + #[doc = ""] + #[doc = $desc] + #[doc = "
"] + pub fn $mode(cx: &mut Context) { + cx.editor.autoinfo = Some(Info::key( + $name, + vec![$((vec![$($key.parse().unwrap()),+], $desc)),+], + )); + use helix_core::hashmap; + let mut map = hashmap! { + $($($key.parse::().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),* + }; + cx.on_next_key_mode(map); } }; - {$name:literal, $cx:expr, $($key:expr; $keyp:pat => $func:expr; $desc:literal),+,} => { - $cx.editor.autoinfo = Some(Info::key( - $name, - vec![ - $( - (vec![$key], $desc), - )+ - ], - )); - $cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; - match event { - $( - $keyp => $func(cx), - )+ - _ => {} - } - }) - } } -fn space_mode(cx: &mut Context) { - mode_info! { - "space mode", cx, - key!('f'); key!('f') => file_picker; "file picker", - key!('b'); key!('b') => buffer_picker; "buffer picker", - key!('s'); key!('s') => symbol_picker; "symbol picker", - key!('w'); key!('w') => window_mode; "window mode", - key!('y'); key!('y') => yank_joined_to_clipboard; "yank joined to clipboard", - key!('Y'); key!('Y') => yank_main_selection_to_clipboard; "yank main selection to clipboard", - key!('p'); key!('p') => paste_clipboard_after; "paste system clipboard after selections", - key!('P'); key!('P') => paste_clipboard_before; "paste system clipboard before selections", - key!('R'); key!('R') => replace_selections_with_clipboard; "replace selections with clipboard", - key!(' '); key!(' ') => keep_primary_selection; "keep primary selection", - } +mode_info! { + space_mode, "space mode", + /// file picker + "f" => file_picker, + /// buffer picker + "b" => buffer_picker, + /// symbol picker + "s" => symbol_picker, + /// window mode + "w" => window_mode, + /// yank joined to clipboard + "y" => yank_joined_to_clipboard, + /// yank main selection to clipboard + "Y" => yank_main_selection_to_clipboard, + /// paste system clipboard after selections + "p" => paste_clipboard_after, + /// paste system clipboard before selections + "P" => paste_clipboard_before, + /// replace selections with clipboard + "R" => replace_selections_with_clipboard, + /// keep primary selection + "space" => keep_primary_selection, } // TODO: generated, delete it later -// fn space_mode(cx: &mut Context) { +// /// space mode +// /// +// /// | key | desc | +// /// |-----|------| +// /// | f | file picker | +// /// | b | buffer picker | +// /// ... +// pub fn space_mode(cx: &mut Context) { // cx.editor.autoinfo = Some(Info::key( -// "space", +// "space mode", // vec![ -// (vec![key!('f')], "file picker"), -// (vec![key!('b')], "buffer picker"), -// (vec![key!('s')], "symbol picker"), -// (vec![key!('w')], "window mode"), -// (vec![key!('y')], "yank joined to clipboard"), -// (vec![key!('Y')], "yank main selection to clipboard"), -// (vec![key!('p')], "paste clipboard after"), -// (vec![key!('P')], "paste clipboard before"), -// (vec![key!('R')], "replace selections with clipboard"), -// (vec![key!(' ')], "keep primary selection"), +// (vec!["f".parse().unwrap()], "file picker"), +// (vec!["b".parse().unwrap()], "buffer picker"), +// (vec!["s".parse().unwrap()], "symbol picker"), +// (vec!["w".parse().unwrap()], "window mode"), +// (vec!["y".parse().unwrap()], "yank joined to clipboard"), +// (vec!["Y".parse().unwrap()], "yank main selection to clipboard"), +// (vec!["p".parse().unwrap()], "paste system clipboard after selections"), +// (vec!["P".parse().unwrap()], "paste system clipboard before selections"), +// (vec!["R".parse().unwrap()], "replace selections with clipboard"), +// (vec![" ".parse().unwrap()], "keep primary selection"), // ], // )); -// cx.on_next_key(move |cx, event| { -// cx.editor.autoinfo = None; -// match event { -// key!('f') => file_picker(cx), -// key!('b') => buffer_picker(cx), -// key!('s') => symbol_picker(cx), -// key!('w') => window_mode(cx), -// key!('y') => yank_joined_to_clipboard(cx), -// key!('Y') => yank_main_selection_to_clipboard(cx), -// key!('p') => paste_clipboard_after(cx), -// key!('P') => paste_clipboard_before(cx), -// key!('R') => replace_selections_with_clipboard(cx), -// // key!(' ') => toggle_alternate_buffer(cx), -// // TODO: temporary since space mode took its old key -// key!(' ') => keep_primary_selection(cx), -// _ => {} -// } -// }) +// let mut map: HashMap = HashMap::with_capacity(10); +// map.insert("f".parse().unwrap(), file_picker); +// map.insert("b".parse().unwrap(), buffer_picker); +// map.insert("s".parse().unwrap(), symbol_picker); +// map.insert("w".parse().unwrap(), window_mode); +// map.insert("y".parse().unwrap(), yank_joined_to_clipboard); +// map.insert("Y".parse().unwrap(), yank_main_selection_to_clipboard); +// map.insert("p".parse().unwrap(), paste_clipboard_after); +// map.insert("P".parse().unwrap(), paste_clipboard_before); +// map.insert("R".parse().unwrap(), replace_selections_with_clipboard); +// map.insert(" ".parse().unwrap(), keep_primary_selection); +// cx.on_next_key_mode(map); // } fn view_mode(cx: &mut Context) { From 64f83dfcbd3e093a31b2ef6bd787161ea8904583 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Thu, 1 Jul 2021 09:33:44 +0800 Subject: [PATCH 14/28] Support infobox doc gen on stable release --- helix-term/src/commands.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9d3a5df9..b6f3c11f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3426,7 +3426,13 @@ macro_rules! mode_info { #[doc = ""] $( #[doc = ""] From 5977b07e197cc6ef9051dd34a28b9fe28e01e966 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Fri, 2 Jul 2021 09:46:28 +0800 Subject: [PATCH 15/28] Reduce calculation and improve pattern in infobox - switch to use static OnceCell to calculate Info once - pass Vec<(&[KeyEvent], &str)> rather than Vec<(Vec, &str)> - expr -> tt to allow using | as separator, make it more like match --- helix-term/src/commands.rs | 21 ++++++++++----------- helix-term/src/keymap.rs | 7 ++----- helix-term/src/ui/info.rs | 2 +- helix-view/src/editor.rs | 2 +- helix-view/src/info.rs | 8 ++++---- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b6f3c11f..351ec1fb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -34,7 +34,6 @@ use movement::Movement; use crate::{ compositor::{self, Component, Compositor}, - key, ui::{self, Picker, Popup, Prompt, PromptEvent}, }; @@ -48,7 +47,7 @@ use std::{ path::{Path, PathBuf}, }; -use once_cell::sync::Lazy; +use once_cell::sync::{Lazy, OnceCell}; use serde::de::{self, Deserialize, Deserializer}; pub struct Context<'a> { @@ -3414,13 +3413,11 @@ fn select_register(cx: &mut Context) { } macro_rules! mode_info { - // TODO: how to use one expr for both pat and expr? - // TODO: how to use replaced function name as str at compile time? - // TODO: extend to support multiple keys, but first solve the other two + // TODO: reuse $mode for $stat (@join $first:expr $(,$rest:expr)*) => { concat!($first, $(", ", $rest),*) }; - {$mode:ident, $name:literal, $(#[doc = $desc:literal] $($key:expr),+ => $func:expr),+,} => { + {$mode:ident, $stat:ident, $name:literal, $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,} => { #[doc = $name] #[doc = ""] #[doc = "
keydesc
"] - #[doc = mode_info!(@join $($key),+)] + // TODO switch to this once we use rust 1.54 + // right now it will produce multiple rows + // #[doc = mode_info!(@join $($key),+)] + $( + #[doc = $key] + )+ + // <- #[doc = ""] #[doc = $desc] #[doc = "
"] @@ -3439,12 +3436,14 @@ macro_rules! mode_info { )+ #[doc = "
keydesc
"] pub fn $mode(cx: &mut Context) { - cx.editor.autoinfo = Some(Info::key( + static $stat: OnceCell = OnceCell::new(); + cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key( $name, - vec![$((vec![$($key.parse().unwrap()),+], $desc)),+], - )); + vec![$((&[$($key.parse().unwrap()),+], $desc)),+], + ))); use helix_core::hashmap; - let mut map = hashmap! { + // TODO: try and convert this to match later + let map = hashmap! { $($($key.parse::().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),* }; cx.on_next_key_mode(map); @@ -3453,7 +3452,7 @@ macro_rules! mode_info { } mode_info! { - space_mode, "space mode", + space_mode, SPACE_MODE, "space mode", /// file picker "f" => file_picker, /// buffer picker diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index ef4a2138..3cd540ea 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,11 +1,7 @@ pub use crate::commands::Command; use crate::config::Config; use helix_core::hashmap; -use helix_view::{ - document::Mode, - input::KeyEvent, - keyboard::{KeyCode, KeyModifiers}, -}; +use helix_view::{document::Mode, input::KeyEvent}; use serde::Deserialize; use std::{ collections::HashMap, @@ -352,6 +348,7 @@ pub fn merge_keys(mut config: Config) -> Config { #[test] fn merge_partial_keys() { + use helix_view::keyboard::{KeyCode, KeyModifiers}; let config = Config { keys: Keymaps(hashmap! { Mode::Normal => hashmap! { diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index 87c2c213..c6f8db43 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -1,5 +1,5 @@ use crate::compositor::{Component, Context}; -use helix_view::graphics::{Margin, Rect, Style}; +use helix_view::graphics::Rect; use helix_view::info::Info; use tui::buffer::Buffer as Surface; use tui::widgets::{Block, Borders, Widget}; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index b006a124..4f01cce4 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -33,7 +33,7 @@ pub struct Editor { pub syn_loader: Arc, pub theme_loader: Arc, - pub autoinfo: Option, + pub autoinfo: Option<&'static Info>, pub status_msg: Option<(String, Severity)>, } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 0eaab783..92c10351 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -16,7 +16,7 @@ pub struct Info { } impl Info { - pub fn key(title: &'static str, body: Vec<(Vec, &'static str)>) -> Info { + pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info { let keymaps_width: u16 = body .iter() .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::() - 2) @@ -25,11 +25,11 @@ impl Info { let mut text = String::new(); let mut width = 0; let height = body.len() as u16; - for (mut keyevents, desc) in body { - let keyevent = keyevents.remove(0); + for (keyevents, desc) in body { + let keyevent = keyevents[0]; let mut left = keymaps_width - keyevent.width() as u16; write!(text, "{}", keyevent).ok(); - for keyevent in keyevents { + for keyevent in &keyevents[1..] { write!(text, ", {}", keyevent).ok(); left -= 2 + keyevent.width() as u16; } From bbbbfa9bcfb7b46d2ca2c8ac85891b1393bfe401 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sat, 3 Jul 2021 23:39:26 +0800 Subject: [PATCH 16/28] Goto mode use infobox In the meantime, change gm to gc. Remove extra space in mode title. --- helix-term/src/commands.rs | 445 ++++++++++++++++++------------------- helix-term/src/keymap.rs | 12 +- 2 files changed, 226 insertions(+), 231 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 351ec1fb..cae474d3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -165,17 +165,12 @@ impl Command { move_char_right, move_line_up, move_line_down, - move_line_end, - move_line_start, - move_first_nonwhitespace, move_next_word_start, move_prev_word_start, move_next_word_end, move_next_long_word_start, move_prev_long_word_start, move_next_long_word_end, - move_file_start, - move_file_end, extend_next_word_start, extend_prev_word_start, extend_next_word_end, @@ -187,7 +182,6 @@ impl Command { find_prev_char, extend_till_prev_char, extend_prev_char, - extend_first_nonwhitespace, replace, page_up, page_down, @@ -197,8 +191,6 @@ impl Command { extend_char_right, extend_line_up, extend_line_down, - extend_line_end, - extend_line_start, select_all, select_regex, split_selection, @@ -229,11 +221,17 @@ impl Command { goto_definition, goto_type_definition, goto_implementation, + goto_file_start, + goto_file_end, goto_reference, goto_first_diag, goto_last_diag, goto_next_diag, goto_prev_diag, + goto_line_start, + goto_line_end, + goto_line_end_newline, + goto_first_nonwhitespace, signature_help, insert_tab, insert_newline, @@ -360,7 +358,7 @@ fn move_line_down(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn move_line_end(cx: &mut Context) { +fn goto_line_end(cx: &mut Context) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id).transform(|range| { @@ -371,13 +369,33 @@ fn move_line_end(cx: &mut Context) { let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1); let pos = range.head.max(pos).max(text.line_to_char(line)); + Range::new( + match doc.mode { + Mode::Normal | Mode::Insert => pos, + Mode::Select => range.anchor, + }, + pos, + ) + }); + + doc.set_selection(view.id, selection); +} + +fn goto_line_end_newline(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let selection = doc.selection(view.id).transform(|range| { + let text = doc.text(); + let line = text.char_to_line(range.head); + + let pos = line_end_char_index(&text.slice(..), line); Range::new(pos, pos) }); doc.set_selection(view.id, selection); } -fn move_line_start(cx: &mut Context) { +fn goto_line_start(cx: &mut Context) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id).transform(|range| { @@ -386,13 +404,19 @@ fn move_line_start(cx: &mut Context) { // adjust to start of the line let pos = text.line_to_char(line); - Range::new(pos, pos) + Range::new( + match doc.mode { + Mode::Normal => range.anchor, + Mode::Select | Mode::Insert => pos, + }, + pos, + ) }); doc.set_selection(view.id, selection); } -fn move_first_nonwhitespace(cx: &mut Context) { +fn goto_first_nonwhitespace(cx: &mut Context) { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id).transform(|range| { @@ -401,7 +425,14 @@ fn move_first_nonwhitespace(cx: &mut Context) { if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) { let pos = pos + text.line_to_char(line_idx); - Range::new(pos, pos) + Range::new( + match doc.mode { + Mode::Normal => pos, + Mode::Select => range.anchor, + Mode::Insert => unreachable!(), + }, + pos, + ) } else { range } @@ -410,6 +441,37 @@ fn move_first_nonwhitespace(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn goto_window_top(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref + let line = (view.first_line + scrolloff).min(view.last_line(doc).saturating_sub(scrolloff)); + let pos = doc.text().line_to_char(line); + + doc.set_selection(view.id, Selection::point(pos)); +} + +fn goto_window_center(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref + let line = view.first_line + (view.area.height as usize / 2); + let line = line.min(view.last_line(doc).saturating_sub(scrolloff)); + let pos = doc.text().line_to_char(line); + + doc.set_selection(view.id, Selection::point(pos)); +} + +fn goto_window_bottom(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref + let line = view.last_line(doc).saturating_sub(scrolloff); + let pos = doc.text().line_to_char(line); + + doc.set_selection(view.id, Selection::point(pos)); +} + // TODO: move vs extend could take an extra type Extend/Move that would // Range::new(if Move { pos } if Extend { range.anchor }, pos) // since these all really do the same thing @@ -486,13 +548,13 @@ fn move_next_long_word_end(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn move_file_start(cx: &mut Context) { +fn goto_file_start(cx: &mut Context) { push_jump(cx.editor); let (view, doc) = current!(cx.editor); doc.set_selection(view.id, Selection::point(0)); } -fn move_file_end(cx: &mut Context) { +fn goto_file_end(cx: &mut Context) { push_jump(cx.editor); let (view, doc) = current!(cx.editor); let text = doc.text(); @@ -671,24 +733,6 @@ fn extend_prev_char(cx: &mut Context) { ) } -fn extend_first_nonwhitespace(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line_idx = text.char_to_line(range.head); - - if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) { - let pos = pos + text.line_to_char(line_idx); - Range::new(range.anchor, pos) - } else { - range - } - }); - - doc.set_selection(view.id, selection); -} - fn replace(cx: &mut Context) { let mut buf = [0u8; 4]; // To hold utf8 encoded char. @@ -834,38 +878,6 @@ fn extend_line_down(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn extend_line_end(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line = text.char_to_line(range.head); - - let pos = line_end_char_index(&text.slice(..), line); - let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1); - let pos = range.head.max(pos).max(text.line_to_char(line)); - - Range::new(range.anchor, pos) - }); - - doc.set_selection(view.id, selection); -} - -fn extend_line_start(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line = text.char_to_line(range.head); - - // adjust to start of the line - let pos = text.line_to_char(line); - Range::new(range.anchor, pos) - }); - - doc.set_selection(view.id, selection); -} - fn select_all(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -1940,7 +1952,7 @@ fn symbol_picker(cx: &mut Context) { // I inserts at the first nonwhitespace character of each line with a selection fn prepend_to_line(cx: &mut Context) { - move_first_nonwhitespace(cx); + goto_first_nonwhitespace(cx); let doc = doc_mut!(cx.editor); enter_insert_mode(doc); } @@ -2106,7 +2118,7 @@ fn push_jump(editor: &mut Editor) { view.jumps.push(jump); } -fn switch_to_last_accessed_file(cx: &mut Context) { +fn goto_last_accessed_file(cx: &mut Context) { let alternate_file = view!(cx.editor).last_accessed_doc; if let Some(alt) = alternate_file { cx.editor.switch(alt, Action::Replace); @@ -2115,7 +2127,15 @@ fn switch_to_last_accessed_file(cx: &mut Context) { } } -fn goto_mode(cx: &mut Context) { +fn select_mode(cx: &mut Context) { + doc_mut!(cx.editor).mode = Mode::Select; +} + +fn exit_select_mode(cx: &mut Context) { + doc_mut!(cx.editor).mode = Mode::Normal; +} + +fn goto_prehook(cx: &mut Context) -> bool { if let Some(count) = cx.count { push_jump(cx.editor); @@ -2123,63 +2143,10 @@ fn goto_mode(cx: &mut Context) { let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2)); let pos = doc.text().line_to_char(line_idx); doc.set_selection(view.id, Selection::point(pos)); - return; + true + } else { + false } - - cx.on_next_key(move |cx, event| { - if let KeyEvent { - code: KeyCode::Char(ch), - .. - } = event - { - // TODO: temporarily show GOTO in the mode list - let doc = doc_mut!(cx.editor); - match (doc.mode, ch) { - (_, 'g') => move_file_start(cx), - (_, 'e') => move_file_end(cx), - (_, 'a') => switch_to_last_accessed_file(cx), - (Mode::Normal, 'h') => move_line_start(cx), - (Mode::Normal, 'l') => move_line_end(cx), - (Mode::Select, 'h') => extend_line_start(cx), - (Mode::Select, 'l') => extend_line_end(cx), - (_, 'd') => goto_definition(cx), - (_, 'y') => goto_type_definition(cx), - (_, 'r') => goto_reference(cx), - (_, 'i') => goto_implementation(cx), - (Mode::Normal, 's') => move_first_nonwhitespace(cx), - (Mode::Select, 's') => extend_first_nonwhitespace(cx), - - (_, 't') | (_, 'm') | (_, 'b') => { - let (view, doc) = current!(cx.editor); - - let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref - - let last_line = view.last_line(doc); - - let line = match ch { - 't' => (view.first_line + scrolloff), - 'm' => (view.first_line + (view.area.height as usize / 2)), - 'b' => last_line.saturating_sub(scrolloff), - _ => unreachable!(), - } - .min(last_line.saturating_sub(scrolloff)); - - let pos = doc.text().line_to_char(line); - - doc.set_selection(view.id, Selection::point(pos)); - } - _ => (), - } - } - }) -} - -fn select_mode(cx: &mut Context) { - doc_mut!(cx.editor).mode = Mode::Select; -} - -fn exit_select_mode(cx: &mut Context) { - doc_mut!(cx.editor).mode = Mode::Normal; } fn goto_impl( @@ -3412,107 +3379,6 @@ fn select_register(cx: &mut Context) { }) } -macro_rules! mode_info { - // TODO: reuse $mode for $stat - (@join $first:expr $(,$rest:expr)*) => { - concat!($first, $(", ", $rest),*) - }; - {$mode:ident, $stat:ident, $name:literal, $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,} => { - #[doc = $name] - #[doc = ""] - #[doc = ""] - $( - #[doc = ""] - )+ - #[doc = "
keydesc
"] - // TODO switch to this once we use rust 1.54 - // right now it will produce multiple rows - // #[doc = mode_info!(@join $($key),+)] - $( - #[doc = $key] - )+ - // <- - #[doc = ""] - #[doc = $desc] - #[doc = "
"] - pub fn $mode(cx: &mut Context) { - static $stat: OnceCell = OnceCell::new(); - cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key( - $name, - vec![$((&[$($key.parse().unwrap()),+], $desc)),+], - ))); - use helix_core::hashmap; - // TODO: try and convert this to match later - let map = hashmap! { - $($($key.parse::().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),* - }; - cx.on_next_key_mode(map); - } - }; -} - -mode_info! { - space_mode, SPACE_MODE, "space mode", - /// file picker - "f" => file_picker, - /// buffer picker - "b" => buffer_picker, - /// symbol picker - "s" => symbol_picker, - /// window mode - "w" => window_mode, - /// yank joined to clipboard - "y" => yank_joined_to_clipboard, - /// yank main selection to clipboard - "Y" => yank_main_selection_to_clipboard, - /// paste system clipboard after selections - "p" => paste_clipboard_after, - /// paste system clipboard before selections - "P" => paste_clipboard_before, - /// replace selections with clipboard - "R" => replace_selections_with_clipboard, - /// keep primary selection - "space" => keep_primary_selection, -} - -// TODO: generated, delete it later -// /// space mode -// /// -// /// | key | desc | -// /// |-----|------| -// /// | f | file picker | -// /// | b | buffer picker | -// /// ... -// pub fn space_mode(cx: &mut Context) { -// cx.editor.autoinfo = Some(Info::key( -// "space mode", -// vec![ -// (vec!["f".parse().unwrap()], "file picker"), -// (vec!["b".parse().unwrap()], "buffer picker"), -// (vec!["s".parse().unwrap()], "symbol picker"), -// (vec!["w".parse().unwrap()], "window mode"), -// (vec!["y".parse().unwrap()], "yank joined to clipboard"), -// (vec!["Y".parse().unwrap()], "yank main selection to clipboard"), -// (vec!["p".parse().unwrap()], "paste system clipboard after selections"), -// (vec!["P".parse().unwrap()], "paste system clipboard before selections"), -// (vec!["R".parse().unwrap()], "replace selections with clipboard"), -// (vec![" ".parse().unwrap()], "keep primary selection"), -// ], -// )); -// let mut map: HashMap = HashMap::with_capacity(10); -// map.insert("f".parse().unwrap(), file_picker); -// map.insert("b".parse().unwrap(), buffer_picker); -// map.insert("s".parse().unwrap(), symbol_picker); -// map.insert("w".parse().unwrap(), window_mode); -// map.insert("y".parse().unwrap(), yank_joined_to_clipboard); -// map.insert("Y".parse().unwrap(), yank_main_selection_to_clipboard); -// map.insert("p".parse().unwrap(), paste_clipboard_after); -// map.insert("P".parse().unwrap(), paste_clipboard_before); -// map.insert("R".parse().unwrap(), replace_selections_with_clipboard); -// map.insert(" ".parse().unwrap(), keep_primary_selection); -// cx.on_next_key_mode(map); -// } - fn view_mode(cx: &mut Context) { cx.on_next_key(move |cx, event| { if let KeyEvent { @@ -3734,3 +3600,132 @@ fn surround_delete(cx: &mut Context) { } }) } + +/// Do nothing, just for modeinfo. +fn noop(_cx: &mut Context) -> bool { + false +} + +/// Generate modeinfo. +/// +/// If prehook returns true then it will stop the rest. +macro_rules! mode_info { + // TODO: reuse $mode for $stat + (@join $first:expr $(,$rest:expr)*) => { + concat!($first, $(", ", $rest),*) + }; + (@name #[doc = $name:literal] $(#[$rest:meta])*) => { + $name + }; + { + #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, + $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+, + } => { + mode_info! { + #[doc = $name] + $(#[$doc])* + $mode, $stat, noop, + $( + #[doc = $desc] + $($key)|+ => $func + ),+, + } + }; + { + #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr, + $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+, + } => { + #[doc = $name] + $(#[$doc])* + #[doc = ""] + #[doc = ""] + $( + #[doc = ""] + )+ + #[doc = "
keydesc
"] + // TODO switch to this once we use rust 1.54 + // right now it will produce multiple rows + // #[doc = mode_info!(@join $($key),+)] + $( + #[doc = $key] + )+ + // <- + #[doc = ""] + #[doc = $desc] + #[doc = "
"] + pub fn $mode(cx: &mut Context) { + if $prehook(cx) { + return; + } + static $stat: OnceCell = OnceCell::new(); + cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key( + $name.trim(), + vec![$((&[$($key.parse().unwrap()),+], $desc)),+], + ))); + use helix_core::hashmap; + // TODO: try and convert this to match later + let map = hashmap! { + $($($key.parse::().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),* + }; + cx.on_next_key_mode(map); + } + }; +} + +mode_info! { + /// space mode + space_mode, SPACE_MODE, + /// file picker + "f" => file_picker, + /// buffer picker + "b" => buffer_picker, + /// symbol picker + "s" => symbol_picker, + /// window mode + "w" => window_mode, + /// yank joined to clipboard + "y" => yank_joined_to_clipboard, + /// yank main selection to clipboard + "Y" => yank_main_selection_to_clipboard, + /// paste system clipboard after selections + "p" => paste_clipboard_after, + /// paste system clipboard before selections + "P" => paste_clipboard_before, + /// replace selections with clipboard + "R" => replace_selections_with_clipboard, + /// keep primary selection + "space" => keep_primary_selection, +} + +mode_info! { + /// goto mode + /// + /// When specified with a count, it will go to that line without entering the mode. + goto_mode, GOTO_MODE, goto_prehook, + /// file start + "g" => goto_file_start, + /// file end + "e" => goto_file_end, + /// line start + "h" => goto_line_start, + /// line end + "l" => goto_line_end, + /// line first non blank + "s" => goto_first_nonwhitespace, + /// definition + "d" => goto_definition, + /// type references + "y" => goto_type_definition, + /// references + "r" => goto_reference, + /// implementation + "i" => goto_implementation, + /// window top + "t" => goto_window_top, + /// window center + "c" => goto_window_center, + /// window bottom + "b" => goto_window_bottom, + /// last accessed file + "a" => goto_last_accessed_file, +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 3cd540ea..6c7a24b1 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -171,8 +171,8 @@ impl Default for Keymaps { key!('r') => Command::replace, key!('R') => Command::replace_with_yanked, - key!(Home) => Command::move_line_start, - key!(End) => Command::move_line_end, + key!(Home) => Command::goto_line_start, + key!(End) => Command::goto_line_end, key!('w') => Command::move_next_word_start, key!('b') => Command::move_prev_word_start, @@ -303,8 +303,8 @@ impl Default for Keymaps { key!('T') => Command::extend_till_prev_char, key!('F') => Command::extend_prev_char, - key!(Home) => Command::extend_line_start, - key!(End) => Command::extend_line_end, + key!(Home) => Command::goto_line_start, + key!(End) => Command::goto_line_end, key!(Esc) => Command::exit_select_mode, ) .into_iter(), @@ -327,8 +327,8 @@ impl Default for Keymaps { key!(Right) => Command::move_char_right, key!(PageUp) => Command::page_up, key!(PageDown) => Command::page_down, - key!(Home) => Command::move_line_start, - key!(End) => Command::move_line_end, + key!(Home) => Command::goto_line_start, + key!(End) => Command::goto_line_end_newline, ctrl!('x') => Command::completion, ctrl!('w') => Command::delete_word_backward, ), From 916362d3a97ddc1b4a630f7d7ba5ae5dc405c21a Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sun, 4 Jul 2021 00:12:02 +0800 Subject: [PATCH 17/28] Info box add horizontal padding --- helix-view/src/info.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 92c10351..f3df50fe 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -17,6 +17,7 @@ pub struct Info { impl Info { pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info { + let (lpad, mpad, rpad) = (1, 2, 1); let keymaps_width: u16 = body .iter() .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::() - 2) @@ -28,18 +29,23 @@ impl Info { for (keyevents, desc) in body { let keyevent = keyevents[0]; let mut left = keymaps_width - keyevent.width() as u16; + for _ in 0..lpad { + text.push(' '); + } write!(text, "{}", keyevent).ok(); for keyevent in &keyevents[1..] { write!(text, ", {}", keyevent).ok(); left -= 2 + keyevent.width() as u16; } - for _ in 0..left { + for _ in 0..left + mpad { text.push(' '); } - if keymaps_width + 2 + (desc.width() as u16) > width { - width = keymaps_width + 2 + desc.width() as u16; + let desc = desc.trim(); + let w = lpad + keymaps_width + mpad + (desc.width() as u16) + rpad; + if w > width { + width = w; } - writeln!(text, " {}", &desc).ok(); + writeln!(text, "{}", desc).ok(); } Info { title, From 6ce303977c4564704ca880353e16c995c7686bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 4 Jul 2021 18:02:42 +0900 Subject: [PATCH 18/28] Revert back to 'gm' top / middle / bottom mnemonic. --- helix-term/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cae474d3..c345f3ba 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -451,7 +451,7 @@ fn goto_window_top(cx: &mut Context) { doc.set_selection(view.id, Selection::point(pos)); } -fn goto_window_center(cx: &mut Context) { +fn goto_window_middle(cx: &mut Context) { let (view, doc) = current!(cx.editor); let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref @@ -3723,7 +3723,7 @@ mode_info! { /// window top "t" => goto_window_top, /// window center - "c" => goto_window_center, + "m" => goto_window_middle, /// window bottom "b" => goto_window_bottom, /// last accessed file From ebccc96cd42d552db7df13249d71177fc016f0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 4 Jul 2021 18:07:58 +0900 Subject: [PATCH 19/28] Factor out goto t/m/b into a single function again --- helix-term/src/commands.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c345f3ba..d8892c9c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -441,35 +441,35 @@ fn goto_first_nonwhitespace(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn goto_window_top(cx: &mut Context) { +fn goto_window(cx: &mut Context, align: Align) { let (view, doc) = current!(cx.editor); let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref - let line = (view.first_line + scrolloff).min(view.last_line(doc).saturating_sub(scrolloff)); - let pos = doc.text().line_to_char(line); - doc.set_selection(view.id, Selection::point(pos)); -} + let last_line = view.last_line(doc); -fn goto_window_middle(cx: &mut Context) { - let (view, doc) = current!(cx.editor); + let line = match align { + Align::Top => (view.first_line + scrolloff), + Align::Center => (view.first_line + (view.area.height as usize / 2)), + Align::Bottom => last_line.saturating_sub(scrolloff), + } + .min(last_line.saturating_sub(scrolloff)); - let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref - let line = view.first_line + (view.area.height as usize / 2); - let line = line.min(view.last_line(doc).saturating_sub(scrolloff)); let pos = doc.text().line_to_char(line); doc.set_selection(view.id, Selection::point(pos)); } -fn goto_window_bottom(cx: &mut Context) { - let (view, doc) = current!(cx.editor); +fn goto_window_top(cx: &mut Context) { + goto_window(cx, Align::Top) +} - let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref - let line = view.last_line(doc).saturating_sub(scrolloff); - let pos = doc.text().line_to_char(line); +fn goto_window_middle(cx: &mut Context) { + goto_window(cx, Align::Center) +} - doc.set_selection(view.id, Selection::point(pos)); +fn goto_window_bottom(cx: &mut Context) { + goto_window(cx, Align::Bottom) } // TODO: move vs extend could take an extra type Extend/Move that would @@ -3722,7 +3722,7 @@ mode_info! { "i" => goto_implementation, /// window top "t" => goto_window_top, - /// window center + /// window middle "m" => goto_window_middle, /// window bottom "b" => goto_window_bottom, From d02bbb7baec13e4d3c7ae0094431f2a20526cf18 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sun, 4 Jul 2021 20:05:01 +0800 Subject: [PATCH 20/28] Fix info panic on small terminal --- helix-term/src/ui/info.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index c6f8db43..e5f20562 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -14,7 +14,12 @@ impl Component for Info { let Info { width, height, .. } = self; let (w, h) = (*width + 2, *height + 2); // -2 to subtract command line + statusline. a bit of a hack, because of splits. - let area = Rect::new(viewport.width - w, viewport.height - h - 2, w, h); + let area = viewport.intersection(Rect::new( + viewport.width.saturating_sub(w), + viewport.height.saturating_sub(h + 2), + w, + h, + )); surface.clear_with(area, style); let Rect { x, y, .. } = block.inner(area); for (y, line) in (y..).zip(self.text.lines()) { From a4e28c6927e59cf08d056e9c9a74be095eae5f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 5 Jul 2021 10:12:34 +0900 Subject: [PATCH 21/28] Implement `X` as extend selection to line bounds --- book/src/keymap.md | 1 + helix-term/src/commands.rs | 21 +++++++++++++++++++++ helix-term/src/keymap.rs | 4 +++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 6b7ccd11..c0c455d3 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -73,6 +73,7 @@ | `Alt-;` | Flip selection cursor and anchor | | `%` | Select entire file | | `x` | Select current line, if already selected, extend to next line | +| `X` | Extend selection to line bounds (line-wise selection) | | | Expand selection to parent syntax node TODO: pick a key | | `J` | join lines inside selection | | `K` | keep selections matching the regex TODO: overlapped by hover help | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d8892c9c..63b91942 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -200,6 +200,7 @@ impl Command { extend_search_next, search_selection, extend_line, + extend_to_line_bounds, delete_selection, change_selection, collapse_selection, @@ -1021,6 +1022,26 @@ fn extend_line(cx: &mut Context) { doc.set_selection(view.id, Selection::single(start, end)); } +fn extend_to_line_bounds(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let text = doc.text(); + let selection = doc.selection(view.id).transform(|range| { + let start = text.line_to_char(text.char_to_line(range.from())); + let end = text + .line_to_char(text.char_to_line(range.to()) + 1) + .saturating_sub(1); + + if range.anchor < range.head { + Range::new(start, end) + } else { + Range::new(end, start) + } + }); + + doc.set_selection(view.id, selection); +} + fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) { // first yank the selection let values: Vec = doc diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 6c7a24b1..c340eb2c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -209,7 +209,9 @@ impl Default for Keymaps { alt!(';') => Command::flip_selections, key!('%') => Command::select_all, key!('x') => Command::extend_line, - // extend_to_whole_line, crop_to_whole_line + key!('x') => Command::extend_line, + key!('X') => Command::extend_to_line_bounds, + // crop_to_whole_line key!('m') => Command::match_mode, From cb4bab890300c747afbd00c3290e6311bf72b0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 5 Jul 2021 10:12:46 +0900 Subject: [PATCH 22/28] Remove outdated comment --- helix-term/src/keymap.rs | 89 ---------------------------------------- 1 file changed, 89 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c340eb2c..d815e006 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -8,95 +8,6 @@ use std::{ ops::{Deref, DerefMut}, }; -// Kakoune-inspired: -// mode = { -// normal = { -// q = record_macro -// w = (next) word -// W = next WORD -// e = end of word -// E = end of WORD -// r = replace -// R = replace with yanked -// t = 'till char -// y = yank -// u = undo -// U = redo -// i = insert -// I = INSERT (start of line) -// o = open below (insert on new line below) -// O = open above (insert on new line above) -// p = paste (before cursor) -// P = PASTE (after cursor) -// ` = -// [ = select to text object start (alt = select whole object) -// ] = select to text object end -// { = extend to inner object start -// } = extend to inner object end -// a = append -// A = APPEND (end of line) -// s = split -// S = select -// d = delete() -// f = find_char() -// g = goto (gg, G, gc, gd, etc) -// -// h = move_char_left(n) || arrow-left = move_char_left(n) -// j = move_line_down(n) || arrow-down = move_line_down(n) -// k = move_line_up(n) || arrow_up = move_line_up(n) -// l = move_char_right(n) || arrow-right = move_char_right(n) -// : = command line -// ; = collapse selection to cursor -// " = use register -// ` = convert case? (to lower) (alt = swap case) -// ~ = convert to upper case -// . = repeat last command -// \ = disable hook? -// / = search -// > = indent -// < = deindent -// % = select whole buffer (in vim = jump to matching bracket) -// * = search pattern in selection -// ( = rotate main selection backward -// ) = rotate main selection forward -// - = trim selections? (alt = merge contiguous sel together) -// @ = convert tabs to spaces -// & = align cursor -// ? = extend to next given regex match (alt = to prev) -// -// in kakoune these are alt-h alt-l / gh gl -// select from curs to begin end / move curs to begin end -// 0 = start of line -// ^ = start of line(first non blank char) || Home = start of line(first non blank char) -// $ = end of line || End = end of line -// -// z = save selections -// Z = restore selections -// x = select line -// X = extend line -// c = change selected text -// C = copy selection? -// v = view menu (viewport manipulation) -// b = select to previous word start -// B = select to previous WORD start -// -// -// -// -// -// -// = = align? -// + = -// } -// -// gd = goto definition -// gr = goto reference -// [d = previous diagnostic -// d] = next diagnostic -// [D = first diagnostic -// D] = last diagnostic -// } - #[macro_export] macro_rules! key { ($key:ident) => { From b72c6204e530987d825acb8b27667085b1c95158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 5 Jul 2021 10:17:26 +0900 Subject: [PATCH 23/28] fix: When calculating relative path, expand tilde last --- helix-view/src/document.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f85ded11..fdb6f8c3 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1049,14 +1049,12 @@ impl Document { let cwdir = std::env::current_dir().expect("couldn't determine current directory"); self.path.as_ref().map(|path| { - let path = fold_home_dir(path); - if path.is_relative() { - path + let path = if path.is_relative() { + path.as_path() } else { - path.strip_prefix(cwdir) - .map(|p| p.to_path_buf()) - .unwrap_or(path) - } + path.strip_prefix(cwdir).unwrap_or(path.as_path()) + }; + fold_home_dir(path) }) } From 48481db8ca3a19c704825adb72a667c4266e9370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 5 Jul 2021 10:26:51 +0900 Subject: [PATCH 24/28] fix: Make path absolute before normalizing :open ../file.txt failed before because .. would be stripped --- helix-view/src/document.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index fdb6f8c3..2ab1602e 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -70,7 +70,6 @@ pub enum IndentStyle { } pub struct Document { - // rope + selection pub(crate) id: DocumentId, text: Rope, pub(crate) selections: HashMap, @@ -408,12 +407,13 @@ pub fn normalize_path(path: &Path) -> PathBuf { /// This function is used instead of `std::fs::canonicalize` because we don't want to verify /// here if the path exists, just normalize it's components. pub fn canonicalize_path(path: &Path) -> std::io::Result { - let normalized = normalize_path(path); - if normalized.is_absolute() { - Ok(normalized) + let path = if path.is_relative() { + std::env::current_dir().map(|current_dir| current_dir.join(path))? } else { - std::env::current_dir().map(|current_dir| current_dir.join(normalized)) - } + path.to_path_buf() + }; + + Ok(normalize_path(&path)) } use helix_lsp::lsp; From fc34efea124c23285770f9bd2e23dbcb905a6965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 5 Jul 2021 10:34:48 +0900 Subject: [PATCH 25/28] appease clippy --- helix-view/src/document.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 2ab1602e..a2bd1c41 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1049,10 +1049,9 @@ impl Document { let cwdir = std::env::current_dir().expect("couldn't determine current directory"); self.path.as_ref().map(|path| { - let path = if path.is_relative() { - path.as_path() - } else { - path.strip_prefix(cwdir).unwrap_or(path.as_path()) + let mut path = path.as_path(); + if path.is_absolute() { + path = path.strip_prefix(cwdir).unwrap_or(path) }; fold_home_dir(path) }) From 4952d6f80154665b50f23d055a4f3bc0ab8ac330 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Mon, 5 Jul 2021 00:11:07 -0700 Subject: [PATCH 26/28] Fix phantom lines in some CRLF files. Fixes #415. The issue was that cursor highlighting wasn't extending to encompass the entire CRLF grapheme, and therefore ended up splitting it. This presumably was messing up other grapheme rendering as well, and this fixes that as well. --- helix-term/src/ui/editor.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ad4f73bc..ef13004c 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -8,7 +8,7 @@ use crate::{ use helix_core::{ coords_at_pos, - graphemes::ensure_grapheme_boundary, + graphemes::{ensure_grapheme_boundary, next_grapheme_boundary}, syntax::{self, HighlightEvent}, LineEnding, Position, Range, }; @@ -187,19 +187,24 @@ impl EditorView { (cursor_scope, selection_scope) }; + let cursor_end = next_grapheme_boundary(text, range.head); // Used in every case below. + if range.head == range.anchor { - spans.push((cursor_scope, range.head..range.head + 1)); + spans.push((cursor_scope, range.head..cursor_end)); continue; } let reverse = range.head < range.anchor; if reverse { - spans.push((cursor_scope, range.head..range.head + 1)); - spans.push((selection_scope, range.head + 1..range.anchor + 1)); + spans.push((cursor_scope, range.head..cursor_end)); + spans.push(( + selection_scope, + cursor_end..next_grapheme_boundary(text, range.anchor), + )); } else { spans.push((selection_scope, range.anchor..range.head)); - spans.push((cursor_scope, range.head..range.head + 1)); + spans.push((cursor_scope, range.head..cursor_end)); } } From aa70362d2052fcf1cb689d9bb93e12b8de5298c6 Mon Sep 17 00:00:00 2001 From: wesh Date: Sun, 4 Jul 2021 14:24:33 +0200 Subject: [PATCH 27/28] Add julia support (LSP not working) --- languages.toml | 25 ++++ runtime/queries/julia/folds.scm | 11 ++ runtime/queries/julia/highlights.scm | 180 +++++++++++++++++++++++++++ runtime/queries/julia/injections.scm | 5 + runtime/queries/julia/locals.scm | 59 +++++++++ 5 files changed, 280 insertions(+) create mode 100644 runtime/queries/julia/folds.scm create mode 100644 runtime/queries/julia/highlights.scm create mode 100644 runtime/queries/julia/injections.scm create mode 100644 runtime/queries/julia/locals.scm diff --git a/languages.toml b/languages.toml index f7564c88..19f81347 100644 --- a/languages.toml +++ b/languages.toml @@ -165,6 +165,31 @@ roots = [] indent = { tab-width = 4, unit = "\t" } +[[language]] +name = "julia" +scope = "source.julia" +injection-regex = "julia" +file-types = ["jl"] +roots = [] + +args = [ + "--startup-file=no", + "--history-file=no", + "-e", + """ + using LanguageServer; + using Pkg; + import StaticLint; + import SymbolServer; + env_path = dirname(Pkg.Types.Context().env.project_file); + + server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); + server.runlinter = true; + run(server); + """, + ] +indent = { tab-width = 2, unit = " " } + # [[language]] # name = "haskell" # scope = "source.haskell" diff --git a/runtime/queries/julia/folds.scm b/runtime/queries/julia/folds.scm new file mode 100644 index 00000000..91eede5f --- /dev/null +++ b/runtime/queries/julia/folds.scm @@ -0,0 +1,11 @@ +[ + (module_definition) + (struct_definition) + (macro_definition) + (function_definition) + (compound_expression) ; begin blocks + (let_statement) + (if_statement) + (for_statement) + (while_statement) +] @fold diff --git a/runtime/queries/julia/highlights.scm b/runtime/queries/julia/highlights.scm new file mode 100644 index 00000000..a53dabe5 --- /dev/null +++ b/runtime/queries/julia/highlights.scm @@ -0,0 +1,180 @@ +(identifier) @variable +;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) +;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables + ;(match? @type "^[A-Z][^_]")) +((identifier) @constant + (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) + +[ + (triple_string) + (string) +] @string + +(string + prefix: (identifier) @constant.builtin) + +(macro_identifier) @function.macro +(macro_identifier (identifier) @function.macro) ; for any one using the variable highlight +(macro_definition + name: (identifier) @function.macro + ["macro" "end" @keyword]) + +(field_expression + (identifier) + (identifier) @field .) + +(function_definition + name: (identifier) @function) +(call_expression + (identifier) @function) +(call_expression + (field_expression (identifier) @method .)) +(broadcast_call_expression + (identifier) @function) +(broadcast_call_expression + (field_expression (identifier) @method .)) +(parameter_list + (identifier) @parameter) +(parameter_list + (optional_parameter . + (identifier) @parameter)) +(typed_parameter + (identifier) @parameter + (identifier) @type) +(type_parameter_list + (identifier) @type) +(typed_parameter + (identifier) @parameter + (parameterized_identifier) @type) +(function_expression + . (identifier) @parameter) +(spread_parameter) @parameter +(spread_parameter + (identifier) @parameter) +(named_argument + . (identifier) @parameter) +(argument_list + (typed_expression + (identifier) @parameter + (identifier) @type)) +(argument_list + (typed_expression + (identifier) @parameter + (parameterized_identifier) @type)) + +;; Symbol expressions (:my-wanna-be-lisp-keyword) +(quote_expression + (identifier)) @symbol + +;; Parsing error! foo (::Type) get's parsed as two quote expressions +(argument_list + (quote_expression + (quote_expression + (identifier) @type))) + +(type_argument_list + (identifier) @type) +(parameterized_identifier (_)) @type +(argument_list + (typed_expression . (identifier) @parameter)) + +(typed_expression + (identifier) @type .) +(typed_expression + (parameterized_identifier) @type .) + +(struct_definition + name: (identifier) @type) + +(number) @number +(range_expression + (identifier) @number + (eq? @number "end")) +(range_expression + (_ + (identifier) @number + (eq? @number "end"))) +(coefficient_expression + (number) + (identifier) @constant.builtin) + +;; TODO: operators. +;; Those are a bit difficult to implement since the respective nodes are hidden right now (_power_operator) +;; and heavily use Unicode chars (support for those are bad in vim/lua regexes) +;[; + ;(power_operator); + ;(times_operator); + ;(plus_operator); + ;(arrow_operator); + ;(comparison_operator); + ;(assign_operator); +;] @operator ; + +"end" @keyword + +(if_statement + ["if" "end"] @conditional) +(elseif_clause + ["elseif"] @conditional) +(else_clause + ["else"] @conditional) +(ternary_expression + ["?" ":"] @conditional) + +(function_definition ["function" "end"] @keyword.function) + +(comment) @comment + +[ + "const" + "return" + "macro" + "struct" + "primitive" + "type" +] @keyword + +((identifier) @keyword (#any-of? @keyword "global" "local")) + +(compound_expression + ["begin" "end"] @keyword) +(try_statement + ["try" "end" ] @exception) +(finally_clause + "finally" @exception) +(catch_clause + "catch" @exception) +(quote_statement + ["quote" "end"] @keyword) +(let_statement + ["let" "end"] @keyword) +(for_statement + ["for" "end"] @repeat) +(while_statement + ["while" "end"] @repeat) +(break_statement) @repeat +(continue_statement) @repeat +(for_binding + "in" @repeat) +(for_clause + "for" @repeat) +(do_clause + ["do" "end"] @keyword) + +(export_statement + ["export"] @include) + +[ + "using" + "module" + "import" +] @include + +((identifier) @include (#eq? @include "baremodule")) + +(((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$")) +(((identifier) @boolean) (eq? @boolean "true")) +(((identifier) @boolean) (eq? @boolean "false")) + +["::" ":" "." "," "..." "!"] @punctuation.delimiter +["[" "]" "(" ")" "{" "}"] @punctuation.bracket diff --git a/runtime/queries/julia/injections.scm b/runtime/queries/julia/injections.scm new file mode 100644 index 00000000..be2412c0 --- /dev/null +++ b/runtime/queries/julia/injections.scm @@ -0,0 +1,5 @@ +; TODO: re-add when markdown is added. +; ((triple_string) @markdown +; (#offset! @markdown 0 3 0 -3)) + +(comment) @comment diff --git a/runtime/queries/julia/locals.scm b/runtime/queries/julia/locals.scm new file mode 100644 index 00000000..f8b34f71 --- /dev/null +++ b/runtime/queries/julia/locals.scm @@ -0,0 +1,59 @@ + +(import_statement + (identifier) @definition.import) +(variable_declaration + (identifier) @definition.var) +(variable_declaration + (tuple_expression + (identifier) @definition.var)) +(for_binding + (identifier) @definition.var) +(for_binding + (tuple_expression + (identifier) @definition.var)) + +(assignment_expression + (tuple_expression + (identifier) @definition.var)) +(assignment_expression + (bare_tuple_expression + (identifier) @definition.var)) +(assignment_expression + (identifier) @definition.var) + +(type_parameter_list + (identifier) @definition.type) +(type_argument_list + (identifier) @definition.type) +(struct_definition + name: (identifier) @definition.type) + +(parameter_list + (identifier) @definition.parameter) +(typed_parameter + (identifier) @definition.parameter + (identifier)) +(function_expression + . (identifier) @definition.parameter) +(argument_list + (typed_expression + (identifier) @definition.parameter + (identifier))) +(spread_parameter + (identifier) @definition.parameter) + +(function_definition + name: (identifier) @definition.function) @scope +(macro_definition + name: (identifier) @definition.macro) @scope + +(identifier) @reference + +[ + (try_statement) + (finally_clause) + (quote_statement) + (let_statement) + (compound_expression) + (for_statement) +] @scope From 3c31f501164080998975883eb6f93c49bd8d3efb Mon Sep 17 00:00:00 2001 From: wesh Date: Mon, 5 Jul 2021 10:58:10 +0200 Subject: [PATCH 28/28] julia language-server line was plain wrong --- languages.toml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/languages.toml b/languages.toml index 19f81347..204a5987 100644 --- a/languages.toml +++ b/languages.toml @@ -171,23 +171,7 @@ scope = "source.julia" injection-regex = "julia" file-types = ["jl"] roots = [] - -args = [ - "--startup-file=no", - "--history-file=no", - "-e", - """ - using LanguageServer; - using Pkg; - import StaticLint; - import SymbolServer; - env_path = dirname(Pkg.Types.Context().env.project_file); - - server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); - server.runlinter = true; - run(server); - """, - ] +language-server = { command = "julia", args = [ "--startup-file=no", "--history-file=no", "-e", "using LanguageServer;using Pkg;import StaticLint;import SymbolServer;env_path = dirname(Pkg.Types.Context().env.project_file);server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, \"\");server.runlinter = true;run(server);" ] } indent = { tab-width = 2, unit = " " } # [[language]]