Merge branch 'master' into great_line_ending_and_cursor_range_cleanup

pull/376/head
Nathan Vegdahl 3 years ago
commit 85d5b399de

36
Cargo.lock generated

@ -317,10 +317,12 @@ dependencies = [
"etcetera", "etcetera",
"helix-syntax", "helix-syntax",
"once_cell", "once_cell",
"quickcheck",
"regex", "regex",
"ropey", "ropey",
"rust-embed", "rust-embed",
"serde", "serde",
"similar",
"smallvec", "smallvec",
"tendril", "tendril",
"toml", "toml",
@ -394,7 +396,6 @@ dependencies = [
"helix-view", "helix-view",
"serde", "serde",
"unicode-segmentation", "unicode-segmentation",
"unicode-width",
] ]
[[package]] [[package]]
@ -692,6 +693,15 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@ -701,6 +711,24 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.9" version = "0.2.9"
@ -872,6 +900,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "similar"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.3" version = "0.4.3"

@ -73,6 +73,7 @@
| `Alt-;` | Flip selection cursor and anchor | | `Alt-;` | Flip selection cursor and anchor |
| `%` | Select entire file | | `%` | Select entire file |
| `x` | Select current line, if already selected, extend to next line | | `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 | | | Expand selection to parent syntax node TODO: pick a key |
| `J` | join lines inside selection | | `J` | join lines inside selection |
| `K` | keep selections matching the regex TODO: overlapped by hover help | | `K` | keep selections matching the regex TODO: overlapped by hover help |
@ -150,7 +151,8 @@ Jumps to various locations.
## Match mode ## Match mode
Enter this mode using `m` from normal mode. See the relavant section 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 | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
@ -158,6 +160,8 @@ in [Usage](./usage.md#surround) for an explanation about surround usage.
| `s` `<char>` | Surround current selection with `<char>` | | `s` `<char>` | Surround current selection with `<char>` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | | `r` `<from><to>` | Replace surround character `<from>` with `<to>` |
| `d` `<char>` | Delete surround character `<char>` | | `d` `<char>` | Delete surround character `<char>` |
| `a` `<object>` | Select around textobject |
| `i` `<object>` | Select inside textobject |
## Object mode ## Object mode

@ -81,6 +81,7 @@ Possible keys:
| `ui.cursor.match` | Matching bracket etc. | | `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection | | `ui.cursor.primary` | Cursor with primary selection |
| `ui.linenr` | | | `ui.linenr` | |
| `ui.linenr.selected` | |
| `ui.statusline` | | | `ui.statusline` | |
| `ui.statusline.inactive` | | | `ui.statusline.inactive` | |
| `ui.popup` | | | `ui.popup` | |

@ -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 - `mr([` to replace the parens with square brackets
Multiple characters are currently not supported, but planned. 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.

@ -31,5 +31,10 @@ regex = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.5" toml = "0.5"
similar = "1.3"
etcetera = "0.3" etcetera = "0.3"
rust-embed = { version = "5.9.0", optional = true } rust-embed = { version = "5.9.0", optional = true }
[dev-dependencies]
quickcheck = { version = "1", default-features = false }

@ -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()
}
}
}

@ -2,6 +2,7 @@ pub mod auto_pairs;
pub mod chars; pub mod chars;
pub mod comment; pub mod comment;
pub mod diagnostic; pub mod diagnostic;
pub mod diff;
pub mod graphemes; pub mod graphemes;
pub mod history; pub mod history;
pub mod indent; pub mod indent;
@ -17,6 +18,7 @@ pub mod selection;
mod state; mod state;
pub mod surround; pub mod surround;
pub mod syntax; pub mod syntax;
pub mod textobject;
mod transaction; mod transaction;
pub mod unicode { pub mod unicode {

@ -176,6 +176,10 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) -
word_move(slice, range, count, WordMotionTarget::PrevLongWordStart) 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 { fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
(0..count).fold(range, |range, _| { (0..count).fold(range, |range, _| {
slice.chars_at(range.head).range_to_target(target, range) slice.chars_at(range.head).range_to_target(target, range)
@ -222,6 +226,7 @@ pub enum WordMotionTarget {
NextWordStart, NextWordStart,
NextWordEnd, NextWordEnd,
PrevWordStart, PrevWordStart,
PrevWordEnd,
// A "Long word" (also known as a WORD in vim/kakoune) is strictly // A "Long word" (also known as a WORD in vim/kakoune) is strictly
// delimited by whitespace, and can consist of punctuation as well // delimited by whitespace, and can consist of punctuation as well
// as alphanumerics. // as alphanumerics.
@ -244,7 +249,9 @@ impl CharHelpers for Chars<'_> {
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range { fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
// Characters are iterated forward or backwards depending on the motion direction. // Characters are iterated forward or backwards depending on the motion direction.
let characters: Box<dyn Iterator<Item = char>> = match target { let characters: Box<dyn Iterator<Item = char>> = match target {
WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => { WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevWordEnd => {
self.next(); self.next();
Box::new(from_fn(|| self.prev())) Box::new(from_fn(|| self.prev()))
} }
@ -253,9 +260,9 @@ impl CharHelpers for Chars<'_> {
// Index advancement also depends on the direction. // Index advancement also depends on the direction.
let advance: &dyn Fn(&mut usize) = match target { let advance: &dyn Fn(&mut usize) = match target {
WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => { WordMotionTarget::PrevWordStart
&|u| *u = u.saturating_sub(1) | WordMotionTarget::PrevLongWordStart
} | WordMotionTarget::PrevWordEnd => &|u| *u = u.saturating_sub(1),
_ => &|u| *u += 1, _ => &|u| *u += 1,
}; };
@ -328,7 +335,7 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>
}; };
match target { match target {
WordMotionTarget::NextWordStart => { WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => {
is_word_boundary(peek, *next_peek) is_word_boundary(peek, *next_peek)
&& (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()) && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace())
} }
@ -978,6 +985,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] #[test]
fn test_behaviour_when_moving_to_end_of_next_long_words() { fn test_behaviour_when_moving_to_end_of_next_long_words() {
let tests = array::IntoIter::new([ let tests = array::IntoIter::new([

@ -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. /// A selection consists of one or more selection ranges.
/// invariant: A selection can never be empty (always contains at least primary range). /// invariant: A selection can never be empty (always contains at least primary range).
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]

@ -41,11 +41,14 @@ pub fn find_nth_pairs_pos(
let (open, close) = get_pair(ch); let (open, close) = get_pair(ch);
let (open_pos, close_pos) = if open == close { let (open_pos, close_pos) = if open == close {
// find_nth* do not consider current character; +1/-1 to include them let prev = search::find_nth_prev(text, open, pos, n, true);
( let next = search::find_nth_next(text, close, pos, n, true);
search::find_nth_prev(text, open, pos + 1, n, true)?, if text.char(pos) == open {
search::find_nth_next(text, close, pos - 1, n, true)?, // cursor is *on* a pair
) next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))?
} else {
(prev?, next?)
}
} else { } else {
( (
find_nth_open_pair(text, open, close, pos, n)?, 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, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21))); assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27))); 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] #[test]

@ -94,6 +94,7 @@ fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::
#[cfg(feature = "embed_runtime")] #[cfg(feature = "embed_runtime")]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> { fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
use std::fmt; use std::fmt;
use std::path::PathBuf;
#[derive(rust_embed::RustEmbed)] #[derive(rust_embed::RustEmbed)]
#[folder = "../runtime/"] #[folder = "../runtime/"]

@ -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
);
}
}
}
}

@ -160,7 +160,11 @@ impl Application {
} }
self.render(); 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.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render(); self.render();
} }

@ -16,6 +16,7 @@ use helix_core::{
use helix_view::{ use helix_view::{
document::{IndentStyle, Mode}, document::{IndentStyle, Mode},
editor::Action, editor::Action,
info::Info,
input::KeyEvent, input::KeyEvent,
keyboard::KeyCode, keyboard::KeyCode,
view::{View, PADDING}, view::{View, PADDING},
@ -38,6 +39,7 @@ use crate::{
use crate::job::{self, Job, Jobs}; use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, TryFutureExt}; use futures_util::{FutureExt, TryFutureExt};
use std::collections::HashMap;
use std::{fmt, future::Future}; use std::{fmt, future::Future};
use std::{ use std::{
@ -45,7 +47,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use once_cell::sync::Lazy; use once_cell::sync::{Lazy, OnceCell};
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
pub struct Context<'a> { pub struct Context<'a> {
@ -74,6 +76,16 @@ impl<'a> Context<'a> {
self.on_next_key_callback = Some(Box::new(on_next_key_callback)); self.on_next_key_callback = Some(Box::new(on_next_key_callback));
} }
#[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] #[inline]
pub fn callback<T, F>( pub fn callback<T, F>(
&mut self, &mut self,
@ -153,17 +165,12 @@ impl Command {
move_char_right, move_char_right,
move_line_up, move_line_up,
move_line_down, move_line_down,
move_line_end,
move_line_start,
move_first_nonwhitespace,
move_next_word_start, move_next_word_start,
move_prev_word_start, move_prev_word_start,
move_next_word_end, move_next_word_end,
move_next_long_word_start, move_next_long_word_start,
move_prev_long_word_start, move_prev_long_word_start,
move_next_long_word_end, move_next_long_word_end,
move_file_start,
move_file_end,
extend_next_word_start, extend_next_word_start,
extend_prev_word_start, extend_prev_word_start,
extend_next_word_end, extend_next_word_end,
@ -175,7 +182,6 @@ impl Command {
find_prev_char, find_prev_char,
extend_till_prev_char, extend_till_prev_char,
extend_prev_char, extend_prev_char,
extend_first_nonwhitespace,
replace, replace,
page_up, page_up,
page_down, page_down,
@ -185,8 +191,6 @@ impl Command {
extend_char_right, extend_char_right,
extend_line_up, extend_line_up,
extend_line_down, extend_line_down,
extend_line_end,
extend_line_start,
select_all, select_all,
select_regex, select_regex,
split_selection, split_selection,
@ -196,6 +200,7 @@ impl Command {
extend_search_next, extend_search_next,
search_selection, search_selection,
extend_line, extend_line,
extend_to_line_bounds,
delete_selection, delete_selection,
change_selection, change_selection,
collapse_selection, collapse_selection,
@ -217,11 +222,17 @@ impl Command {
goto_definition, goto_definition,
goto_type_definition, goto_type_definition,
goto_implementation, goto_implementation,
goto_file_start,
goto_file_end,
goto_reference, goto_reference,
goto_first_diag, goto_first_diag,
goto_last_diag, goto_last_diag,
goto_next_diag, goto_next_diag,
goto_prev_diag, goto_prev_diag,
goto_line_start,
goto_line_end,
goto_line_end_newline,
goto_first_nonwhitespace,
signature_help, signature_help,
insert_tab, insert_tab,
insert_newline, 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); let (view, doc) = current!(cx.editor);
doc.set_selection( doc.set_selection(
view.id, 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 = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
let pos = range.head.max(pos).max(text.line_to_char(line)); 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) Range::new(pos, pos)
}), }),
); );
} }
fn move_line_start(cx: &mut Context) { fn goto_line_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.set_selection( doc.set_selection(
view.id, view.id,
@ -403,12 +435,18 @@ fn move_line_start(cx: &mut Context) {
// adjust to start of the line // adjust to start of the line
let pos = text.line_to_char(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); let (view, doc) = current!(cx.editor);
doc.set_selection( doc.set_selection(
view.id, 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)) { if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
let pos = pos + text.line_to_char(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 { } else {
range 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 // TODO: move vs extend could take an extra type Extend/Move that would
// Range::new(if Move { pos } if Extend { range.anchor }, pos) // Range::new(if Move { pos } if Extend { range.anchor }, pos)
// since these all really do the same thing // 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); push_jump(cx.editor);
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, Selection::point(0)); 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); push_jump(cx.editor);
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let text = doc.text(); 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) { fn replace(cx: &mut Context) {
let mut buf = [0u8; 4]; // To hold utf8 encoded char. 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) { fn select_all(cx: &mut Context) {
let (view, doc) = current!(cx.editor); 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)); 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) { fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let selection = doc.selection(view_id).clone().min_width_1(text); 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] = &[ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "quit", name: "quit",
@ -1763,6 +1827,20 @@ mod cmd {
fun: show_current_directory, fun: show_current_directory,
completer: None, 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(|| { 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 // I inserts at the first nonwhitespace character of each line with a selection
fn prepend_to_line(cx: &mut Context) { fn prepend_to_line(cx: &mut Context) {
move_first_nonwhitespace(cx); goto_first_nonwhitespace(cx);
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
enter_insert_mode(doc); enter_insert_mode(doc);
} }
@ -2124,7 +2202,7 @@ fn push_jump(editor: &mut Editor) {
view.jumps.push(jump); 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; let alternate_file = view!(cx.editor).last_accessed_doc;
if let Some(alt) = alternate_file { if let Some(alt) = alternate_file {
cx.editor.switch(alt, Action::Replace); 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) { fn select_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor); 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) { fn exit_select_mode(cx: &mut Context) {
doc_mut!(cx.editor).mode = Mode::Normal; 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( fn goto_impl(
editor: &mut Editor, editor: &mut Editor,
compositor: &mut Compositor, 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) { fn view_mode(cx: &mut Context) {
cx.on_next_key(move |cx, event| { cx.on_next_key(move |cx, event| {
if let KeyEvent { 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) { fn match_mode(cx: &mut Context) {
let count = cx.count; let count = cx.count;
cx.on_next_key(move |cx, event| { cx.on_next_key(move |cx, event| {
@ -3574,13 +3583,41 @@ fn match_mode(cx: &mut Context) {
's' => surround_add(cx), 's' => surround_add(cx),
'r' => surround_replace(cx), 'r' => surround_replace(cx),
'd' => surround_delete(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) { fn surround_add(cx: &mut Context) {
cx.on_next_key(move |cx, event| { 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,
}

@ -16,9 +16,9 @@ pub struct Job {
#[derive(Default)] #[derive(Default)]
pub struct Jobs { pub struct Jobs {
futures: FuturesUnordered<JobFuture>, pub futures: FuturesUnordered<JobFuture>,
/// These are the ones that need to complete before we exit. /// These are the ones that need to complete before we exit.
wait_futures: FuturesUnordered<JobFuture>, pub wait_futures: FuturesUnordered<JobFuture>,
} }
impl Job { impl Job {
@ -77,11 +77,11 @@ impl Jobs {
} }
} }
pub fn next_job( pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
&mut self, tokio::select! {
) -> impl Future<Output = Option<anyhow::Result<Option<Callback>>>> + '_ { event = self.futures.next() => { event }
future::select(self.futures.next(), self.wait_futures.next()) event = self.wait_futures.next() => { event }
.map(|either| either.factor_first().0) }
} }
pub fn add(&mut self, j: Job) { pub fn add(&mut self, j: Job) {

@ -1,118 +1,25 @@
pub use crate::commands::Command; pub use crate::commands::Command;
use crate::config::Config; use crate::config::Config;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::{ use helix_view::{document::Mode, input::KeyEvent};
document::Mode,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::{Deref, DerefMut}, 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_export]
macro_rules! key { macro_rules! key {
($key:ident) => { ($key:ident) => {
KeyEvent { KeyEvent {
code: KeyCode::$key, code: ::helix_view::keyboard::KeyCode::$key,
modifiers: KeyModifiers::NONE, modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
} }
}; };
($($ch:tt)*) => { ($($ch:tt)*) => {
KeyEvent { KeyEvent {
code: KeyCode::Char($($ch)*), code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: KeyModifiers::NONE, modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
} }
}; };
} }
@ -120,8 +27,8 @@ macro_rules! key {
macro_rules! ctrl { macro_rules! ctrl {
($($ch:tt)*) => { ($($ch:tt)*) => {
KeyEvent { KeyEvent {
code: KeyCode::Char($($ch)*), code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: KeyModifiers::CONTROL, modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
} }
}; };
} }
@ -129,8 +36,8 @@ macro_rules! ctrl {
macro_rules! alt { macro_rules! alt {
($($ch:tt)*) => { ($($ch:tt)*) => {
KeyEvent { KeyEvent {
code: KeyCode::Char($($ch)*), code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: KeyModifiers::ALT, modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
} }
}; };
} }
@ -175,8 +82,8 @@ impl Default for Keymaps {
key!('r') => Command::replace, key!('r') => Command::replace,
key!('R') => Command::replace_with_yanked, key!('R') => Command::replace_with_yanked,
key!(Home) => Command::move_line_start, key!(Home) => Command::goto_line_start,
key!(End) => Command::move_line_end, key!(End) => Command::goto_line_end,
key!('w') => Command::move_next_word_start, key!('w') => Command::move_next_word_start,
key!('b') => Command::move_prev_word_start, key!('b') => Command::move_prev_word_start,
@ -213,7 +120,9 @@ impl Default for Keymaps {
alt!(';') => Command::flip_selections, alt!(';') => Command::flip_selections,
key!('%') => Command::select_all, key!('%') => Command::select_all,
key!('x') => Command::extend_line, 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, key!('m') => Command::match_mode,
@ -307,8 +216,8 @@ impl Default for Keymaps {
key!('T') => Command::extend_till_prev_char, key!('T') => Command::extend_till_prev_char,
key!('F') => Command::extend_prev_char, key!('F') => Command::extend_prev_char,
key!(Home) => Command::extend_line_start, key!(Home) => Command::goto_line_start,
key!(End) => Command::extend_line_end, key!(End) => Command::goto_line_end,
key!(Esc) => Command::exit_select_mode, key!(Esc) => Command::exit_select_mode,
) )
.into_iter(), .into_iter(),
@ -331,8 +240,8 @@ impl Default for Keymaps {
key!(Right) => Command::move_char_right, key!(Right) => Command::move_char_right,
key!(PageUp) => Command::page_up, key!(PageUp) => Command::page_up,
key!(PageDown) => Command::page_down, key!(PageDown) => Command::page_down,
key!(Home) => Command::move_line_start, key!(Home) => Command::goto_line_start,
key!(End) => Command::move_line_end, key!(End) => Command::goto_line_end_newline,
ctrl!('x') => Command::completion, ctrl!('x') => Command::completion,
ctrl!('w') => Command::delete_word_backward, ctrl!('w') => Command::delete_word_backward,
), ),
@ -352,6 +261,7 @@ pub fn merge_keys(mut config: Config) -> Config {
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
use helix_view::keyboard::{KeyCode, KeyModifiers};
let config = Config { let config = Config {
keys: Keymaps(hashmap! { keys: Keymaps(hashmap! {
Mode::Normal => hashmap! { Mode::Normal => hashmap! {

@ -738,6 +738,11 @@ impl Component for EditorView {
self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused); 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 // render status msg
if let Some((status_msg, severity)) = &cx.editor.status_msg { if let Some((status_msg, severity)) = &cx.editor.status_msg {
use helix_view::editor::Severity; use helix_view::editor::Severity;
@ -756,8 +761,7 @@ impl Component for EditorView {
} }
if let Some(completion) = &self.completion { if let Some(completion) = &self.completion {
completion.render(area, surface, cx) completion.render(area, surface, cx);
// render completion here
} }
} }

@ -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);
}
}

@ -1,5 +1,6 @@
mod completion; mod completion;
mod editor; mod editor;
mod info;
mod markdown; mod markdown;
mod menu; mod menu;
mod picker; mod picker;

@ -19,7 +19,6 @@ default = ["crossterm"]
bitflags = "1.0" bitflags = "1.0"
cassowary = "0.3" cassowary = "0.3"
unicode-segmentation = "1.2" unicode-segmentation = "1.2"
unicode-width = "0.1"
crossterm = { version = "0.20", optional = true } crossterm = { version = "0.20", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]} serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.3", path = "../helix-view", features = ["term"] } helix-view = { version = "0.3", path = "../helix-view", features = ["term"] }

@ -2,9 +2,9 @@ use crate::{
backend::Backend, backend::Backend,
buffer::{Buffer, Cell}, buffer::{Buffer, Cell},
}; };
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{CursorKind, Rect}; use helix_view::graphics::{CursorKind, Rect};
use std::{fmt::Write, io}; use std::{fmt::Write, io};
use unicode_width::UnicodeWidthStr;
/// A backend used for the integration tests. /// A backend used for the integration tests.
#[derive(Debug)] #[derive(Debug)]

@ -1,7 +1,7 @@
use crate::text::{Span, Spans}; use crate::text::{Span, Spans};
use helix_core::unicode::width::UnicodeWidthStr;
use std::cmp::min; use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use helix_view::graphics::{Color, Modifier, Rect, Style}; use helix_view::graphics::{Color, Modifier, Rect, Style};

@ -47,10 +47,10 @@
//! ]); //! ]);
//! ``` //! ```
use helix_core::line_ending::str_is_line_ending; use helix_core::line_ending::str_is_line_ending;
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::Style; use helix_view::graphics::Style;
use std::borrow::Cow; use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A grapheme associated to a style. /// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

@ -7,9 +7,9 @@ use crate::{
Block, Widget, Block, Widget,
}, },
}; };
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{Rect, Style}; use helix_view::graphics::{Rect, Style};
use std::iter; use std::iter;
use unicode_width::UnicodeWidthStr;
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
match alignment { match alignment {

@ -1,7 +1,7 @@
use crate::text::StyledGrapheme; use crate::text::StyledGrapheme;
use helix_core::line_ending::str_is_line_ending; use helix_core::line_ending::str_is_line_ending;
use helix_core::unicode::width::UnicodeWidthStr;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const NBSP: &str = "\u{00a0}"; const NBSP: &str = "\u{00a0}";

@ -9,9 +9,9 @@ use cassowary::{
WeightedRelation::*, WeightedRelation::*,
{Expression, Solver}, {Expression, Solver},
}; };
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{Rect, Style}; use helix_view::graphics::{Rect, Style};
use std::collections::HashMap; use std::collections::HashMap;
use unicode_width::UnicodeWidthStr;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
/// ///

@ -70,7 +70,6 @@ pub enum IndentStyle {
} }
pub struct Document { pub struct Document {
// rope + selection
pub(crate) id: DocumentId, pub(crate) id: DocumentId,
text: Rope, text: Rope,
pub(crate) selections: HashMap<ViewId, Selection>, pub(crate) selections: HashMap<ViewId, Selection>,
@ -307,6 +306,19 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
Ok(()) 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 /// Like std::mem::replace() except it allows the replacement value to be mapped from the
/// original value. /// original value.
fn take_with<T, F>(mut_ref: &mut T, closure: F) 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 /// 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. /// here if the path exists, just normalize it's components.
pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> { pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
let normalized = normalize_path(path); let path = if path.is_relative() {
if normalized.is_absolute() { std::env::current_dir().map(|current_dir| current_dir.join(path))?
Ok(normalized)
} else { } else {
std::env::current_dir().map(|current_dir| current_dir.join(normalized)) path.to_path_buf()
} };
Ok(normalize_path(&path))
} }
use helix_lsp::lsp; 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 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)); let mut doc = Self::from(rope, Some(encoding));
@ -458,9 +472,9 @@ impl Document {
doc.detect_language(theme, loader); doc.detect_language(theme, loader);
} }
// Detect indentation style and line ending. // Detect indentation style and set line ending.
doc.detect_indent_style(); 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) 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) { fn detect_indent_style(&mut self) {
// Build a histogram of the indentation *increases* between // Build a histogram of the indentation *increases* between
// subsequent lines, ignoring lines that are all whitespace. // 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"); let cwdir = std::env::current_dir().expect("couldn't determine current directory");
self.path.as_ref().map(|path| { self.path.as_ref().map(|path| {
let path = fold_home_dir(path); let mut path = path.as_path();
if path.is_relative() { if path.is_absolute() {
path path = path.strip_prefix(cwdir).unwrap_or(path)
} else { };
path.strip_prefix(cwdir) fold_home_dir(path)
.map(|p| p.to_path_buf())
.unwrap_or(path)
}
}) })
} }

@ -1,6 +1,7 @@
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
info::Info,
theme::{self, Theme}, theme::{self, Theme},
tree::Tree, tree::Tree,
Document, DocumentId, RegisterSelection, View, ViewId, Document, DocumentId, RegisterSelection, View, ViewId,
@ -32,6 +33,7 @@ pub struct Editor {
pub syn_loader: Arc<syntax::Loader>, pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>, pub theme_loader: Arc<theme::Loader>,
pub autoinfo: Option<&'static Info>,
pub status_msg: Option<(String, Severity)>, pub status_msg: Option<(String, Severity)>,
} }
@ -64,6 +66,7 @@ impl Editor {
theme_loader: themes, theme_loader: themes,
registers: Registers::default(), registers: Registers::default(),
clipboard_provider: get_clipboard_provider(), clipboard_provider: get_clipboard_provider(),
autoinfo: None,
status_msg: None, status_msg: None,
} }
} }

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

@ -1,5 +1,6 @@
//! Input event handling, currently backed by crossterm. //! Input event handling, currently backed by crossterm.
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use helix_core::unicode::width::UnicodeWidthStr;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use std::fmt; use std::fmt;
@ -13,6 +14,32 @@ pub struct KeyEvent {
pub modifiers: KeyModifiers, 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 { impl fmt::Display for KeyEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!( f.write_fmt(format_args!(
@ -34,28 +61,29 @@ impl fmt::Display for KeyEvent {
}, },
))?; ))?;
match self.code { match self.code {
KeyCode::Backspace => f.write_str("backspace")?, KeyCode::Backspace => f.write_str(keys::BACKSPACE)?,
KeyCode::Enter => f.write_str("ret")?, KeyCode::Enter => f.write_str(keys::ENTER)?,
KeyCode::Left => f.write_str("left")?, KeyCode::Left => f.write_str(keys::LEFT)?,
KeyCode::Right => f.write_str("right")?, KeyCode::Right => f.write_str(keys::RIGHT)?,
KeyCode::Up => f.write_str("up")?, KeyCode::Up => f.write_str(keys::UP)?,
KeyCode::Down => f.write_str("down")?, KeyCode::Down => f.write_str(keys::DOWN)?,
KeyCode::Home => f.write_str("home")?, KeyCode::Home => f.write_str(keys::HOME)?,
KeyCode::End => f.write_str("end")?, KeyCode::End => f.write_str(keys::END)?,
KeyCode::PageUp => f.write_str("pageup")?, KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str("pagedown")?, KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str("tab")?, KeyCode::Tab => f.write_str(keys::TAB)?,
KeyCode::BackTab => f.write_str("backtab")?, KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str("del")?, KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str("ins")?, KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str("null")?, KeyCode::Null => f.write_str(keys::NULL)?,
KeyCode::Esc => f.write_str("esc")?, KeyCode::Esc => f.write_str(keys::ESC)?,
KeyCode::Char('<') => f.write_str("lt")?, KeyCode::Char(' ') => f.write_str(keys::SPACE)?,
KeyCode::Char('>') => f.write_str("gt")?, KeyCode::Char('<') => f.write_str(keys::LESS_THAN)?,
KeyCode::Char('+') => f.write_str("plus")?, KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?,
KeyCode::Char('-') => f.write_str("minus")?, KeyCode::Char('+') => f.write_str(keys::PLUS)?,
KeyCode::Char(';') => f.write_str("semicolon")?, KeyCode::Char('-') => f.write_str(keys::MINUS)?,
KeyCode::Char('%') => f.write_str("percent")?, 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::F(i) => f.write_fmt(format_args!("F{}", i))?,
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, 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 { impl std::str::FromStr for KeyEvent {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens: Vec<_> = s.split('-').collect(); let mut tokens: Vec<_> = s.split('-').collect();
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? { let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
"backspace" => KeyCode::Backspace, keys::BACKSPACE => KeyCode::Backspace,
"space" => KeyCode::Char(' '), keys::ENTER => KeyCode::Enter,
"ret" => KeyCode::Enter, keys::LEFT => KeyCode::Left,
"lt" => KeyCode::Char('<'), keys::RIGHT => KeyCode::Right,
"gt" => KeyCode::Char('>'), keys::UP => KeyCode::Up,
"plus" => KeyCode::Char('+'), keys::DOWN => KeyCode::Down,
"minus" => KeyCode::Char('-'), keys::HOME => KeyCode::Home,
"semicolon" => KeyCode::Char(';'), keys::END => KeyCode::End,
"percent" => KeyCode::Char('%'), keys::PAGEUP => KeyCode::PageUp,
"left" => KeyCode::Left, keys::PAGEDOWN => KeyCode::PageDown,
"right" => KeyCode::Right, keys::TAB => KeyCode::Tab,
"up" => KeyCode::Down, keys::BACKTAB => KeyCode::BackTab,
"home" => KeyCode::Home, keys::DELETE => KeyCode::Delete,
"end" => KeyCode::End, keys::INSERT => KeyCode::Insert,
"pageup" => KeyCode::PageUp, keys::NULL => KeyCode::Null,
"pagedown" => KeyCode::PageDown, keys::ESC => KeyCode::Esc,
"tab" => KeyCode::Tab, keys::SPACE => KeyCode::Char(' '),
"backtab" => KeyCode::BackTab, keys::LESS_THAN => KeyCode::Char('<'),
"del" => KeyCode::Delete, keys::GREATER_THAN => KeyCode::Char('>'),
"ins" => KeyCode::Insert, keys::PLUS => KeyCode::Char('+'),
"null" => KeyCode::Null, keys::MINUS => KeyCode::Char('-'),
"esc" => KeyCode::Esc, keys::SEMICOLON => KeyCode::Char(';'),
keys::PERCENT => KeyCode::Char('%'),
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
function if function.len() > 1 && function.starts_with('F') => { function if function.len() > 1 && function.starts_with('F') => {
let function: String = function.chars().skip(1).collect(); let function: String = function.chars().skip(1).collect();

@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod graphics; pub mod graphics;
pub mod info;
pub mod input; pub mod input;
pub mod keyboard; pub mod keyboard;
pub mod register_selection; pub mod register_selection;

@ -165,6 +165,15 @@ roots = []
indent = { tab-width = 4, unit = "\t" } 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]] # [[language]]
# name = "haskell" # name = "haskell"
# scope = "source.haskell" # scope = "source.haskell"

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

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

@ -0,0 +1,5 @@
; TODO: re-add when markdown is added.
; ((triple_string) @markdown
; (#offset! @markdown 0 3 0 -3))
(comment) @comment

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

@ -29,16 +29,26 @@
"warning" = { fg = "#e5c07b", modifiers = ['bold'] } "warning" = { fg = "#e5c07b", modifiers = ['bold'] }
"error" = { fg = "#e06c75", modifiers = ['bold'] } "error" = { fg = "#e06c75", modifiers = ['bold'] }
"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
"ui.background" = { fg = "#ABB2BF", bg = "#282C34" } "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" = { fg = "#4B5263", modifiers = ['dim'] }
"ui.linenr.selected" = { fg = "#ABB2BF" } "ui.linenr.selected" = { fg = "#ABB2BF" }
"ui.popup" = { bg = "#3E4452" }
"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" } "ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
"ui.statusline.inactive" = { fg = "#ABB2Bf", bg = "#2C323C" } "ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" }
"ui.selection" = { bg = "#3E4452" }
"ui.text" = { fg = "#ABB2BF", bg = "#282C34" } "ui.text" = { fg = "#ABB2BF", bg = "#282C34" }
"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] } "ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
"ui.help" = { bg = "#3E4452" }
"ui.popup" = { bg = "#3E4452" }
"ui.window" = { 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" }

Loading…
Cancel
Save