Merge branch 'master' into great_line_ending_and_cursor_range_cleanup
This commit is contained in:
commit
85d5b399de
37 changed files with 1495 additions and 361 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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` | |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
70
helix-core/src/diff.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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/"]
|
||||
|
|
318
helix-core/src/textobject.rs
Normal file
318
helix-core/src/textobject.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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
30
helix-term/src/ui/info.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
mod completion;
|
||||
mod editor;
|
||||
mod info;
|
||||
mod markdown;
|
||||
mod menu;
|
||||
mod picker;
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}";
|
||||
|
||||
|
|
|
@ -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`].
|
||||
///
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
57
helix-view/src/info.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
11
runtime/queries/julia/folds.scm
Normal file
11
runtime/queries/julia/folds.scm
Normal 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
|
180
runtime/queries/julia/highlights.scm
Normal file
180
runtime/queries/julia/highlights.scm
Normal 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
|
5
runtime/queries/julia/injections.scm
Normal file
5
runtime/queries/julia/injections.scm
Normal file
|
@ -0,0 +1,5 @@
|
|||
; TODO: re-add when markdown is added.
|
||||
; ((triple_string) @markdown
|
||||
; (#offset! @markdown 0 3 0 -3))
|
||||
|
||||
(comment) @comment
|
59
runtime/queries/julia/locals.scm
Normal file
59
runtime/queries/julia/locals.scm
Normal 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
|
|
@ -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" }
|
||||
|
||||
|
|
Loading…
Reference in a new issue