aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock36
-rw-r--r--book/src/keymap.md6
-rw-r--r--book/src/themes.md1
-rw-r--r--book/src/usage.md16
-rw-r--r--helix-core/Cargo.toml5
-rw-r--r--helix-core/src/diff.rs70
-rw-r--r--helix-core/src/lib.rs2
-rw-r--r--helix-core/src/movement.rs99
-rw-r--r--helix-core/src/selection.rs10
-rw-r--r--helix-core/src/surround.rs18
-rw-r--r--helix-core/src/syntax.rs1
-rw-r--r--helix-core/src/textobject.rs318
-rw-r--r--helix-term/src/application.rs6
-rw-r--r--helix-term/src/commands.rs478
-rw-r--r--helix-term/src/job.rs14
-rw-r--r--helix-term/src/keymap.rs128
-rw-r--r--helix-term/src/ui/editor.rs8
-rw-r--r--helix-term/src/ui/info.rs30
-rw-r--r--helix-term/src/ui/mod.rs1
-rw-r--r--helix-tui/Cargo.toml1
-rw-r--r--helix-tui/src/backend/test.rs2
-rw-r--r--helix-tui/src/buffer.rs2
-rw-r--r--helix-tui/src/text.rs2
-rw-r--r--helix-tui/src/widgets/paragraph.rs2
-rw-r--r--helix-tui/src/widgets/reflow.rs2
-rw-r--r--helix-tui/src/widgets/table.rs2
-rw-r--r--helix-view/src/document.rs84
-rw-r--r--helix-view/src/editor.rs3
-rw-r--r--helix-view/src/info.rs57
-rw-r--r--helix-view/src/input.rs165
-rw-r--r--helix-view/src/lib.rs1
-rw-r--r--languages.toml9
-rw-r--r--runtime/queries/julia/folds.scm11
-rw-r--r--runtime/queries/julia/highlights.scm180
-rw-r--r--runtime/queries/julia/injections.scm5
-rw-r--r--runtime/queries/julia/locals.scm59
-rw-r--r--runtime/themes/onedark.toml22
37 files changed, 1495 insertions, 361 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 473ae8c8..2cd202f3 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",
@@ -394,7 +396,6 @@ dependencies = [
"helix-view",
"serde",
"unicode-segmentation",
- "unicode-width",
]
[[package]]
@@ -693,6 +694,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -702,6 +712,24 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -873,6 +901,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/book/src/keymap.md b/book/src/keymap.md
index 0265fe6d..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 |
@@ -150,7 +151,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 +160,8 @@ in [Usage](./usage.md#surround) for an explanation about surround usage.
| `s` `<char>` | Surround current selection with `<char>` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` |
| `d` `<char>` | Delete surround character `<char>` |
+| `a` `<object>` | Select around textobject |
+| `i` `<object>` | Select inside textobject |
## Object mode
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` | |
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, `<alt-a>` in kakoune)
+- `mi` - Select inside the object (`vi` in vim, `<alt-i>` 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/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<Change> = 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..3684a93e 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;
@@ -17,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 62311ee4..bc56f9a4 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -176,6 +176,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)
@@ -222,6 +226,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.
@@ -244,7 +249,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<dyn Iterator<Item = char>> = match target {
- WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => {
+ WordMotionTarget::PrevWordStart
+ | WordMotionTarget::PrevLongWordStart
+ | WordMotionTarget::PrevWordEnd => {
self.next();
Box::new(from_fn(|| self.prev()))
}
@@ -253,9 +260,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,
};
@@ -328,7 +335,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())
}
@@ -979,6 +986,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([
("Basic forward motion from the start of a word to the end of it",
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 6ca798a6..64ff51d8 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -216,6 +216,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/surround.rs b/helix-core/src/surround.rs
index 61981d6e..52f60cab 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 {
+ // cursor 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/syntax.rs b/helix-core/src/syntax.rs
index d4379a8e..84a5f9bd 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<String, std::io::
#[cfg(feature = "embed_runtime")]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
use std::fmt;
+ use std::path::PathBuf;
#[derive(rust_embed::RustEmbed)]
#[folder = "../runtime/"]
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
new file mode 100644
index 00000000..fbf66256
--- /dev/null
+++ b/helix-core/src/textobject.rs
@@ -0,0 +1,318 @@
+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),
+ (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),
+ ],
+ ),
+ (
+ "(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/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/commands.rs b/helix-term/src/commands.rs
index 5ab0926a..fbeae5ff 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},
@@ -38,6 +39,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::{
@@ -45,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> {
@@ -75,6 +77,16 @@ impl<'a> Context<'a> {
}
#[inline]
+ pub fn on_next_key_mode(&mut self, map: HashMap<KeyEvent, fn(&mut Context)>) {
+ self.on_next_key(move |cx, event| {
+ cx.editor.autoinfo = None;
+ if let Some(func) = map.get(&event) {
+ func(cx);
+ }
+ });
+ }
+
+ #[inline]
pub fn callback<T, F>(
&mut self,
call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
@@ -153,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,
@@ -175,7 +182,6 @@ impl Command {
find_prev_char,
extend_till_prev_char,
extend_prev_char,
- extend_first_nonwhitespace,
replace,
page_up,
page_down,
@@ -185,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,
@@ -196,6 +200,7 @@ impl Command {
extend_search_next,
search_selection,
extend_line,
+ extend_to_line_bounds,
delete_selection,
change_selection,
collapse_selection,
@@ -217,11 +222,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,
@@ -376,7 +387,7 @@ fn move_line_down(cx: &mut Context) {
);
}
-fn move_line_end(cx: &mut Context) {
+fn goto_line_end(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
doc.set_selection(
view.id,
@@ -388,12 +399,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,
+ )
+ }),
+ );
+}
+
+fn goto_line_end_newline(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().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)
}),
);
}
-fn move_line_start(cx: &mut Context) {
+fn goto_line_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
doc.set_selection(
view.id,
@@ -403,12 +435,18 @@ 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 | Mode::Insert => pos,
+ Mode::Select => range.anchor,
+ },
+ pos,
+ )
}),
);
}
-fn move_first_nonwhitespace(cx: &mut Context) {
+fn goto_first_nonwhitespace(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
doc.set_selection(
view.id,
@@ -418,7 +456,13 @@ 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 | Mode::Insert => pos,
+ Mode::Select => range.anchor,
+ },
+ pos,
+ )
} else {
range
}
@@ -426,6 +470,37 @@ fn move_first_nonwhitespace(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 last_line = view.last_line(doc);
+
+ 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 pos = doc.text().line_to_char(line);
+
+ doc.set_selection(view.id, Selection::point(pos));
+}
+
+fn goto_window_top(cx: &mut Context) {
+ goto_window(cx, Align::Top)
+}
+
+fn goto_window_middle(cx: &mut Context) {
+ goto_window(cx, Align::Center)
+}
+
+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
// Range::new(if Move { pos } if Extend { range.anchor }, pos)
// since these all really do the same thing
@@ -497,13 +572,13 @@ fn move_next_long_word_end(cx: &mut Context) {
);
}
-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();
@@ -683,24 +758,6 @@ fn extend_prev_char(cx: &mut Context) {
)
}
-fn extend_first_nonwhitespace(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
- doc.set_selection(
- view.id,
- doc.selection(view.id).clone().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
- }
- }),
- );
-}
-
fn replace(cx: &mut Context) {
let mut buf = [0u8; 4]; // To hold utf8 encoded char.
@@ -880,38 +937,6 @@ fn extend_line_down(cx: &mut Context) {
);
}
-fn extend_line_end(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
- doc.set_selection(
- view.id,
- doc.selection(view.id).clone().transform(|range| {
- let text = doc.text().slice(..);
- let line = text.char_to_line(range.head);
-
- let pos = line_end_char_index(&text, line);
- let pos = graphemes::nth_prev_grapheme_boundary(text, pos, 1);
- let pos = range.head.max(pos).max(text.line_to_char(line));
-
- Range::new(range.anchor, pos)
- }),
- );
-}
-
-fn extend_line_start(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
- doc.set_selection(
- view.id,
- doc.selection(view.id).clone().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)
- }),
- );
-}
-
fn select_all(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@@ -1055,6 +1080,27 @@ 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);
+
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text();
+ 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)
+ }
+ }),
+ );
+}
+
fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
let text = doc.text().slice(..);
let selection = doc.selection(view_id).clone().min_width_1(text);
@@ -1580,6 +1626,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",
@@ -1763,6 +1827,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<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -1955,7 +2033,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);
}
@@ -2124,7 +2202,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);
@@ -2133,65 +2211,6 @@ fn switch_to_last_accessed_file(cx: &mut Context) {
}
}
-fn goto_mode(cx: &mut Context) {
- if let Some(count) = cx.count {
- push_jump(cx.editor);
-
- let (view, doc) = current!(cx.editor);
- 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;
- }
-
- 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) {
let (view, doc) = current!(cx.editor);
@@ -2211,13 +2230,27 @@ fn select_mode(cx: &mut Context) {
}),
);
- doc.mode = Mode::Select;
+ 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);
+
+ let (view, doc) = current!(cx.editor);
+ let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(1));
+ let pos = doc.text().line_to_char(line_idx);
+ doc.set_selection(view.id, Selection::point(pos));
+ true
+ } else {
+ false
+ }
+}
+
fn goto_impl(
editor: &mut Editor,
compositor: &mut Compositor,
@@ -3457,33 +3490,6 @@ 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),
- _ => (),
- }
- }
- })
-}
-
fn view_mode(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let KeyEvent {
@@ -3559,6 +3565,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| {
@@ -3574,13 +3583,41 @@ 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);
+
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text().slice(..);
+ 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,
+ }
+ }),
+ );
+ }
+ })
+}
fn surround_add(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
@@ -3671,3 +3708,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 = "<table><tr><th>key</th><th>desc</th></tr><tbody>"]
+ $(
+ #[doc = "<tr><td>"]
+ // 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 = "</td><td>"]
+ #[doc = $desc]
+ #[doc = "</td></tr>"]
+ )+
+ #[doc = "</tbody></table>"]
+ pub fn $mode(cx: &mut Context) {
+ if $prehook(cx) {
+ return;
+ }
+ static $stat: OnceCell<Info> = 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::<KeyEvent>().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 middle
+ "m" => goto_window_middle,
+ /// window bottom
+ "b" => goto_window_bottom,
+ /// last accessed file
+ "a" => goto_last_accessed_file,
+}
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<JobFuture>,
+ pub futures: FuturesUnordered<JobFuture>,
/// These are the ones that need to complete before we exit.
- wait_futures: FuturesUnordered<JobFuture>,
+ pub wait_futures: FuturesUnordered<JobFuture>,
}
impl Job {
@@ -77,11 +77,11 @@ impl Jobs {
}
}
- pub fn next_job(
- &mut self,
- ) -> impl Future<Output = Option<anyhow::Result<Option<Callback>>>> + '_ {
- future::select(self.futures.next(), self.wait_futures.next())
- .map(|either| either.factor_first().0)
+ pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
+ tokio::select! {
+ event = self.futures.next() => { event }
+ event = self.wait_futures.next() => { event }
+ }
}
pub fn add(&mut self, j: Job) {
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 53588a2b..d815e006 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,118 +1,25 @@
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,
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) => {
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 +27,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 +36,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,
}
};
}
@@ -175,8 +82,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,
@@ -213,7 +120,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,
@@ -307,8 +216,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(),
@@ -331,8 +240,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,
),
@@ -352,6 +261,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/editor.rs b/helix-term/src/ui/editor.rs
index dab654ad..d374d9b6 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -738,6 +738,11 @@ 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);
+ cx.editor.autoinfo = Some(info);
+ }
+
// render status msg
if let Some((status_msg, severity)) = &cx.editor.status_msg {
use helix_view::editor::Severity;
@@ -756,8 +761,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..e5f20562
--- /dev/null
+++ b/helix-term/src/ui/info.rs
@@ -0,0 +1,30 @@
+use crate::compositor::{Component, Context};
+use helix_view::graphics::Rect;
+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 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 = 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()) {
+ surface.set_string(x, y, line, style);
+ }
+ 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-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/src/document.rs b/helix-view/src/document.rs
index 86f3dfb8..b917b902 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<ViewId, Selection>,
@@ -307,6 +306,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<T, F>(mut_ref: &mut T, closure: F)
@@ -395,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<PathBuf> {
- 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;
@@ -448,7 +461,8 @@ impl Document {
}
let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
- let (rope, encoding) = from_reader(&mut file, encoding)?;
+ let (mut rope, encoding) = from_reader(&mut file, encoding)?;
+ let line_ending = with_line_ending(&mut rope);
let mut doc = Self::from(rope, Some(encoding));
@@ -458,9 +472,9 @@ impl Document {
doc.detect_language(theme, loader);
}
- // Detect indentation style and line ending.
+ // Detect indentation style and set line ending.
doc.detect_indent_style();
- doc.line_ending = auto_detect_line_ending(&doc.text).unwrap_or(DEFAULT_LINE_ENDING);
+ doc.line_ending = line_ending;
Ok(doc)
}
@@ -578,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.
@@ -996,14 +1049,11 @@ 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
- } else {
- path.strip_prefix(cwdir)
- .map(|p| p.to_path_buf())
- .unwrap_or(path)
- }
+ let mut path = path.as_path();
+ if path.is_absolute() {
+ path = path.strip_prefix(cwdir).unwrap_or(path)
+ };
+ fold_home_dir(path)
})
}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index a16cc50f..4f01cce4 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<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>,
+ pub autoinfo: Option<&'static Info>,
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..f3df50fe
--- /dev/null
+++ b/helix-view/src/info.rs
@@ -0,0 +1,57 @@
+use crate::input::KeyEvent;
+use helix_core::unicode::width::UnicodeWidthStr;
+use std::fmt::Write;
+
+#[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<(&[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::<u16>() - 2)
+ .max()
+ .unwrap();
+ let mut text = String::new();
+ let mut width = 0;
+ let height = body.len() as u16;
+ 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 + mpad {
+ text.push(' ');
+ }
+ let desc = desc.trim();
+ let w = lpad + keymaps_width + mpad + (desc.width() as u16) + rpad;
+ if w > width {
+ width = w;
+ }
+ 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..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;
@@ -13,6 +14,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 +61,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 +91,83 @@ impl fmt::Display for KeyEvent {
}
}
+impl UnicodeWidthStr for KeyEvent {
+ fn width(&self) -> usize {
+ use helix_core::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<Self, Self::Err> {
let mut tokens: Vec<_> = s.split('-').collect();
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
- "backspace" => KeyCode::Backspace,
- "space" => KeyCode::Char(' '),
- "ret" => KeyCode::Enter,
- "lt" => KeyCode::Char('<'),
- "gt" => KeyCode::Char('>'),
- "plus" => KeyCode::Char('+'),
- "minus" => KeyCode::Char('-'),
- "semicolon" => KeyCode::Char(';'),
- "percent" => KeyCode::Char('%'),
- "left" => KeyCode::Left,
- "right" => KeyCode::Right,
- "up" => KeyCode::Down,
- "home" => KeyCode::Home,
- "end" => KeyCode::End,
- "pageup" => KeyCode::PageUp,
- "pagedown" => KeyCode::PageDown,
- "tab" => KeyCode::Tab,
- "backtab" => KeyCode::BackTab,
- "del" => KeyCode::Delete,
- "ins" => KeyCode::Insert,
- "null" => KeyCode::Null,
- "esc" => KeyCode::Esc,
+ 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;
diff --git a/languages.toml b/languages.toml
index f7564c88..204a5987 100644
--- a/languages.toml
+++ b/languages.toml
@@ -165,6 +165,15 @@ roots = []
indent = { tab-width = 4, unit = "\t" }
+[[language]]
+name = "julia"
+scope = "source.julia"
+injection-regex = "julia"
+file-types = ["jl"]
+roots = []
+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]]
# 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
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" }
+