Merge branch 'master' into great_line_ending_and_cursor_range_cleanup

This commit is contained in:
Nathan Vegdahl 2021-07-05 20:27:49 -07:00
commit 85d5b399de
37 changed files with 1495 additions and 361 deletions

36
Cargo.lock generated
View file

@ -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]]
@ -692,6 +693,15 @@ dependencies = [
"unicase",
]
[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
]
[[package]]
name = "quote"
version = "1.0.9"
@ -701,6 +711,24 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.9"
@ -872,6 +900,12 @@ dependencies = [
"libc",
]
[[package]]
name = "similar"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]]
name = "slab"
version = "0.4.3"

View file

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

View file

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

View file

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

View file

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

70
helix-core/src/diff.rs Normal file
View file

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

View file

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

View file

@ -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())
}
@ -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]
fn test_behaviour_when_moving_to_end_of_next_long_words() {
let tests = array::IntoIter::new([

View file

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

View file

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

View file

@ -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/"]

View file

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

View file

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

View file

@ -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> {
@ -74,6 +76,16 @@ impl<'a> Context<'a> {
self.on_next_key_callback = Some(Box::new(on_next_key_callback));
}
#[inline]
pub fn on_next_key_mode(&mut self, map: HashMap<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,
@ -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,
}

View file

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

View file

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

View file

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

30
helix-term/src/ui/info.rs Normal file
View file

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

View file

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

View file

@ -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"] }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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`].
///

View file

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

View file

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

57
helix-view/src/info.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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