on master help sign in
Merge branch 'master' into great_line_ending_and_cursor_range_cleanup
85d5b39Nathan Vegdahl
Cargo.lock+35−1
@@ -317,10 +317,12 @@ dependencies = [
317 "etcetera",
317 "etcetera",
318 "helix-syntax",
318 "helix-syntax",
319 "once_cell",
319 "once_cell",
320+ "quickcheck",
320 "regex",
321 "regex",
321 "ropey",
322 "ropey",
322 "rust-embed",
323 "rust-embed",
323 "serde",
324 "serde",
325+ "similar",
324 "smallvec",
326 "smallvec",
325 "tendril",
327 "tendril",
326 "toml",
328 "toml",
@@ -394,7 +396,6 @@ dependencies = [
394 "helix-view",
396 "helix-view",
395 "serde",
397 "serde",
396 "unicode-segmentation",
398 "unicode-segmentation",
397− "unicode-width",
398]
399]
399
400
400[[package]]
401[[package]]
@@ -692,6 +693,15 @@ dependencies = [
692 "unicase",
693 "unicase",
693]
694]
694
695
696+[[package]]
697+name = "quickcheck"
698+version = "1.0.3"
699+source = "registry+https://github.com/rust-lang/crates.io-index"
700+checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
701+dependencies = [
702+ "rand",
703+]
704+
695[[package]]
705[[package]]
696name = "quote"
706name = "quote"
697version = "1.0.9"
707version = "1.0.9"
@@ -701,6 +711,24 @@ dependencies = [
701 "proc-macro2",
711 "proc-macro2",
702]
712]
703
713
714+[[package]]
715+name = "rand"
716+version = "0.8.4"
717+source = "registry+https://github.com/rust-lang/crates.io-index"
718+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
719+dependencies = [
720+ "rand_core",
721+]
722+
723+[[package]]
724+name = "rand_core"
725+version = "0.6.3"
726+source = "registry+https://github.com/rust-lang/crates.io-index"
727+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
728+dependencies = [
729+ "getrandom",
730+]
731+
704[[package]]
732[[package]]
705name = "redox_syscall"
733name = "redox_syscall"
706version = "0.2.9"
734version = "0.2.9"
@@ -872,6 +900,12 @@ dependencies = [
872 "libc",
900 "libc",
873]
901]
874
902
903+[[package]]
904+name = "similar"
905+version = "1.3.0"
906+source = "registry+https://github.com/rust-lang/crates.io-index"
907+checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
908+
875[[package]]
909[[package]]
876name = "slab"
910name = "slab"
877version = "0.4.3"
911version = "0.4.3"
book/src/keymap.md+5−1
@@ -73,6 +73,7 @@
73| `Alt-;` | Flip selection cursor and anchor |
73| `Alt-;` | Flip selection cursor and anchor |
74| `%` | Select entire file |
74| `%` | Select entire file |
75| `x` | Select current line, if already selected, extend to next line |
75| `x` | Select current line, if already selected, extend to next line |
76+| `X` | Extend selection to line bounds (line-wise selection) |
76| | Expand selection to parent syntax node TODO: pick a key |
77| | Expand selection to parent syntax node TODO: pick a key |
77| `J` | join lines inside selection |
78| `J` | join lines inside selection |
78| `K` | keep selections matching the regex TODO: overlapped by hover help |
79| `K` | keep selections matching the regex TODO: overlapped by hover help |
@@ -150,7 +151,8 @@ Jumps to various locations.
150## Match mode
151## Match mode
151
152
152Enter this mode using `m` from normal mode. See the relavant section
153Enter this mode using `m` from normal mode. See the relavant section
153−in [Usage](./usage.md#surround) for an explanation about surround usage.
154+in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
155+and [textobject](./usage.md#textobject) usage.
154
156
155| Key | Description |
157| Key | Description |
156| ----- | ----------- |
158| ----- | ----------- |
@@ -158,6 +160,8 @@ in [Usage](./usage.md#surround) for an explanation about surround usage.
158| `s` `<char>` | Surround current selection with `<char>` |
160| `s` `<char>` | Surround current selection with `<char>` |
159| `r` `<from><to>` | Replace surround character `<from>` with `<to>` |
161| `r` `<from><to>` | Replace surround character `<from>` with `<to>` |
160| `d` `<char>` | Delete surround character `<char>` |
162| `d` `<char>` | Delete surround character `<char>` |
163+| `a` `<object>` | Select around textobject |
164+| `i` `<object>` | Select inside textobject |
161
165
162## Object mode
166## Object mode
163
167
book/src/themes.md+1
@@ -81,6 +81,7 @@ Possible keys:
81| `ui.cursor.match` | Matching bracket etc. |
81| `ui.cursor.match` | Matching bracket etc. |
82| `ui.cursor.primary` | Cursor with primary selection |
82| `ui.cursor.primary` | Cursor with primary selection |
83| `ui.linenr` | |
83| `ui.linenr` | |
84+| `ui.linenr.selected` | |
84| `ui.statusline` | |
85| `ui.statusline` | |
85| `ui.statusline.inactive` | |
86| `ui.statusline.inactive` | |
86| `ui.popup` | |
87| `ui.popup` | |
book/src/usage.md+16
@@ -24,3 +24,19 @@ It can also act on multiple seletions (yay!). For example, to change every occur
24- `mr([` to replace the parens with square brackets
24- `mr([` to replace the parens with square brackets
25
25
26Multiple characters are currently not supported, but planned.
26Multiple characters are currently not supported, but planned.
27+
28+## Textobjects
29+
30+Currently supported: `word`, `surround`.
31+
32+![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
33+
34+- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
35+- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
36+
37+| Key after `mi` or `ma` | Textobject selected |
38+| --- | --- |
39+| `w` | Word |
40+| `(`, `[`, `'`, etc | Specified surround pairs |
41+
42+Textobjects based on treesitter, like `function`, `class`, etc are planned.
helix-core/Cargo.toml+5
@@ -31,5 +31,10 @@ regex = "1"
31serde = { version = "1.0", features = ["derive"] }
31serde = { version = "1.0", features = ["derive"] }
32toml = "0.5"
32toml = "0.5"
33
33
34+similar = "1.3"
35+
34etcetera = "0.3"
36etcetera = "0.3"
35rust-embed = { version = "5.9.0", optional = true }
37rust-embed = { version = "5.9.0", optional = true }
38+
39+[dev-dependencies]
40+quickcheck = { version = "1", default-features = false }
helix-core/src/diff.rsnew file+70
@@ -0,0 +1,70 @@
1+use ropey::Rope;
2+
3+use crate::{Change, Transaction};
4+
5+/// Compares `old` and `new` to generate a [`Transaction`] describing
6+/// the steps required to get from `old` to `new`.
7+pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
8+ // `similar` only works on contiguous data, so a `Rope` has
9+ // to be temporarily converted into a `String`.
10+ let old_converted = old.to_string();
11+ let new_converted = new.to_string();
12+
13+ // A timeout is set so after 1 seconds, the algorithm will start
14+ // approximating. This is especially important for big `Rope`s or
15+ // `Rope`s that are extremely dissimilar to each other.
16+ //
17+ // Note: Ignore the clippy warning, as the trait bounds of
18+ // `Transaction::change()` require an iterator implementing
19+ // `ExactIterator`.
20+ let mut config = similar::TextDiff::configure();
21+ config.timeout(std::time::Duration::from_secs(1));
22+
23+ let diff = config.diff_chars(&old_converted, &new_converted);
24+
25+ // The current position of the change needs to be tracked to
26+ // construct the `Change`s.
27+ let mut pos = 0;
28+ let changes: Vec<Change> = diff
29+ .ops()
30+ .iter()
31+ .map(|op| op.as_tag_tuple())
32+ .filter_map(|(tag, old_range, new_range)| {
33+ // `old_pos..pos` is equivalent to `start..end` for where
34+ // the change should be applied.
35+ let old_pos = pos;
36+ pos += old_range.end - old_range.start;
37+
38+ match tag {
39+ // Semantically, inserts and replacements are the same thing.
40+ similar::DiffTag::Insert | similar::DiffTag::Replace => {
41+ // This is the text from the `new` rope that should be
42+ // inserted into `old`.
43+ let text: &str = {
44+ let start = new.char_to_byte(new_range.start);
45+ let end = new.char_to_byte(new_range.end);
46+ &new_converted[start..end]
47+ };
48+ Some((old_pos, pos, Some(text.into())))
49+ }
50+ similar::DiffTag::Delete => Some((old_pos, pos, None)),
51+ similar::DiffTag::Equal => None,
52+ }
53+ })
54+ .collect();
55+ Transaction::change(old, changes.into_iter())
56+}
57+
58+#[cfg(test)]
59+mod tests {
60+ use super::*;
61+
62+ quickcheck::quickcheck! {
63+ fn test_compare_ropes(a: String, b: String) -> bool {
64+ let mut old = Rope::from(a);
65+ let new = Rope::from(b);
66+ compare_ropes(&old, &new).apply(&mut old);
67+ old.to_string() == new.to_string()
68+ }
69+ }
70+}
helix-core/src/lib.rs+2
@@ -2,6 +2,7 @@ pub mod auto_pairs;
2pub mod chars;
2pub mod chars;
3pub mod comment;
3pub mod comment;
4pub mod diagnostic;
4pub mod diagnostic;
5+pub mod diff;
5pub mod graphemes;
6pub mod graphemes;
6pub mod history;
7pub mod history;
7pub mod indent;
8pub mod indent;
@@ -17,6 +18,7 @@ pub mod selection;
17mod state;
18mod state;
18pub mod surround;
19pub mod surround;
19pub mod syntax;
20pub mod syntax;
21+pub mod textobject;
20mod transaction;
22mod transaction;
21
23
22pub mod unicode {
24pub mod unicode {
helix-core/src/movement.rs+94−5
@@ -176,6 +176,10 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) -
176 word_move(slice, range, count, WordMotionTarget::PrevLongWordStart)
176 word_move(slice, range, count, WordMotionTarget::PrevLongWordStart)
177}
177}
178
178
179+pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
180+ word_move(slice, range, count, WordMotionTarget::PrevWordEnd)
181+}
182+
179fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
183fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
180 (0..count).fold(range, |range, _| {
184 (0..count).fold(range, |range, _| {
181 slice.chars_at(range.head).range_to_target(target, range)
185 slice.chars_at(range.head).range_to_target(target, range)
@@ -222,6 +226,7 @@ pub enum WordMotionTarget {
222 NextWordStart,
226 NextWordStart,
223 NextWordEnd,
227 NextWordEnd,
224 PrevWordStart,
228 PrevWordStart,
229+ PrevWordEnd,
225 // A "Long word" (also known as a WORD in vim/kakoune) is strictly
230 // A "Long word" (also known as a WORD in vim/kakoune) is strictly
226 // delimited by whitespace, and can consist of punctuation as well
231 // delimited by whitespace, and can consist of punctuation as well
227 // as alphanumerics.
232 // as alphanumerics.
@@ -244,7 +249,9 @@ impl CharHelpers for Chars<'_> {
244 fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
249 fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
245 // Characters are iterated forward or backwards depending on the motion direction.
250 // Characters are iterated forward or backwards depending on the motion direction.
246 let characters: Box<dyn Iterator<Item = char>> = match target {
251 let characters: Box<dyn Iterator<Item = char>> = match target {
252+ WordMotionTarget::PrevWordStart
247− WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => {
253+ | WordMotionTarget::PrevLongWordStart
254+ | WordMotionTarget::PrevWordEnd => {
248 self.next();
255 self.next();
249 Box::new(from_fn(|| self.prev()))
256 Box::new(from_fn(|| self.prev()))
250 }
257 }
@@ -253,9 +260,9 @@ impl CharHelpers for Chars<'_> {
253
260
254 // Index advancement also depends on the direction.
261 // Index advancement also depends on the direction.
255 let advance: &dyn Fn(&mut usize) = match target {
262 let advance: &dyn Fn(&mut usize) = match target {
263+ WordMotionTarget::PrevWordStart
256− WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => {
264+ | WordMotionTarget::PrevLongWordStart
257− &|u| *u = u.saturating_sub(1)
265+ | WordMotionTarget::PrevWordEnd => &|u| *u = u.saturating_sub(1),
258− }
259 _ => &|u| *u += 1,
266 _ => &|u| *u += 1,
260 };
267 };
261
268
@@ -328,7 +335,7 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>
328 };
335 };
329
336
330 match target {
337 match target {
331− WordMotionTarget::NextWordStart => {
338+ WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => {
332 is_word_boundary(peek, *next_peek)
339 is_word_boundary(peek, *next_peek)
333 && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace())
340 && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace())
334 }
341 }
@@ -978,6 +985,88 @@ mod test {
978 }
985 }
979 }
986 }
980
987
988+ #[test]
989+ fn test_behaviour_when_moving_to_end_of_previous_words() {
990+ let tests = array::IntoIter::new([
991+ ("Basic backward motion from the middle of a word",
992+ vec![(1, Range::new(9, 9), Range::new(9, 5))]),
993+ ("Starting from after boundary retreats the anchor",
994+ vec![(1, Range::new(0, 13), Range::new(12, 8))]),
995+ ("Jump to end of a word succeeded by whitespace",
996+ vec![(1, Range::new(10, 10), Range::new(10, 4))]),
997+ (" Jump to start of line from end of word preceded by whitespace",
998+ vec![(1, Range::new(7, 7), Range::new(7, 0))]),
999+ ("Previous anchor is irrelevant for backward motions",
1000+ vec![(1, Range::new(26, 12), Range::new(12, 8))]),
1001+ (" Starting from whitespace moves to first space in sequence",
1002+ vec![(1, Range::new(0, 3), Range::new(3, 0))]),
1003+ ("Test identifiers_with_underscores are considered a single word",
1004+ vec![(1, Range::new(0, 25), Range::new(25, 4))]),
1005+ ("Jumping\n \nback through a newline selects whitespace",
1006+ vec![(1, Range::new(0, 13), Range::new(11, 8))]),
1007+ ("Jumping to start of word from the end selects the whole word",
1008+ vec![(1, Range::new(15, 15), Range::new(15, 10))]),
1009+ ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
1010+ vec![
1011+ (1, Range::new(30, 30), Range::new(30, 21)),
1012+ (1, Range::new(30, 21), Range::new(20, 18)),
1013+ (1, Range::new(20, 18), Range::new(17, 15))
1014+ ]),
1015+
1016+ ("... ... punctuation and spaces behave as expected",
1017+ vec![
1018+ (1, Range::new(0, 10), Range::new(9, 9)),
1019+ (1, Range::new(9, 6), Range::new(5, 3)),
1020+ ]),
1021+ (".._.._ punctuation is not joined by underscores into a single block",
1022+ vec![(1, Range::new(0, 5), Range::new(4, 3))]),
1023+ ("Newlines\n\nare bridged seamlessly.",
1024+ vec![
1025+ (1, Range::new(0, 10), Range::new(7, 0)),
1026+ ]),
1027+ ("Jumping \n\n\n\n\nback from within a newline group selects previous block",
1028+ vec![
1029+ (1, Range::new(0, 13), Range::new(10, 7)),
1030+ ]),
1031+ ("Failed motions do not modify the range",
1032+ vec![
1033+ (0, Range::new(3, 0), Range::new(3, 0)),
1034+ ]),
1035+ ("Multiple motions at once resolve correctly",
1036+ vec![
1037+ (3, Range::new(23, 23), Range::new(15, 8)),
1038+ ]),
1039+ ("Excessive motions are performed partially",
1040+ vec![
1041+ (999, Range::new(40, 40), Range::new(8, 0)),
1042+ ]),
1043+ ("", // Edge case of moving backwards in empty string
1044+ vec![
1045+ (1, Range::new(0, 0), Range::new(0, 0)),
1046+ ]),
1047+ ("\n\n\n\n\n", // Edge case of moving backwards in all newlines
1048+ vec![
1049+ (1, Range::new(0, 0), Range::new(0, 0)),
1050+ ]),
1051+ (" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
1052+ vec![
1053+ (1, Range::new(0, 7), Range::new(6, 4)),
1054+ (1, Range::new(6, 4), Range::new(2, 0)),
1055+ ]),
1056+ ("Test ヒーリクス multibyte characters behave as normal characters",
1057+ vec![
1058+ (1, Range::new(0, 9), Range::new(9, 4)),
1059+ ]),
1060+ ]);
1061+
1062+ for (sample, scenario) in tests {
1063+ for (count, begin, expected_end) in scenario.into_iter() {
1064+ let range = move_prev_word_end(Rope::from(sample).slice(..), begin, count);
1065+ assert_eq!(range, expected_end, "Case failed: [{}]", sample);
1066+ }
1067+ }
1068+ }
1069+
981 #[test]
1070 #[test]
982 fn test_behaviour_when_moving_to_end_of_next_long_words() {
1071 fn test_behaviour_when_moving_to_end_of_next_long_words() {
983 let tests = array::IntoIter::new([
1072 let tests = array::IntoIter::new([
helix-core/src/selection.rs+10
@@ -216,6 +216,16 @@ impl Range {
216 }
216 }
217}
217}
218
218
219+impl From<(usize, usize)> for Range {
220+ fn from(tuple: (usize, usize)) -> Self {
221+ Self {
222+ anchor: tuple.0,
223+ head: tuple.1,
224+ horiz: None,
225+ }
226+ }
227+}
228+
219/// A selection consists of one or more selection ranges.
229/// A selection consists of one or more selection ranges.
220/// invariant: A selection can never be empty (always contains at least primary range).
230/// invariant: A selection can never be empty (always contains at least primary range).
221#[derive(Debug, Clone, PartialEq, Eq)]
231#[derive(Debug, Clone, PartialEq, Eq)]
helix-core/src/surround.rs+13−5
@@ -41,11 +41,14 @@ pub fn find_nth_pairs_pos(
41 let (open, close) = get_pair(ch);
41 let (open, close) = get_pair(ch);
42
42
43 let (open_pos, close_pos) = if open == close {
43 let (open_pos, close_pos) = if open == close {
44− // find_nth* do not consider current character; +1/-1 to include them
45− (
46− search::find_nth_prev(text, open, pos + 1, n, true)?,
44+ let prev = search::find_nth_prev(text, open, pos, n, true);
47− search::find_nth_next(text, close, pos - 1, n, true)?,
45+ let next = search::find_nth_next(text, close, pos, n, true);
48− )
46+ if text.char(pos) == open {
47+ // cursor is *on* a pair
48+ next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))?
49+ } else {
50+ (prev?, next?)
51+ }
49 } else {
52 } else {
50 (
53 (
51 find_nth_open_pair(text, open, close, pos, n)?,
54 find_nth_open_pair(text, open, close, pos, n)?,
@@ -198,6 +201,11 @@ mod test {
198 assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
201 assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
199 assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
202 assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
200 assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
203 assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
204+ // cursor on the quotes
205+ assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), Some((10, 15)));
206+ // this is the best we can do since opening and closing pairs are same
207+ assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
208+ assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
201 }
209 }
202
210
203 #[test]
211 #[test]
helix-core/src/syntax.rs+1
@@ -94,6 +94,7 @@ fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::
94#[cfg(feature = "embed_runtime")]
94#[cfg(feature = "embed_runtime")]
95fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
95fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
96 use std::fmt;
96 use std::fmt;
97+ use std::path::PathBuf;
97
98
98 #[derive(rust_embed::RustEmbed)]
99 #[derive(rust_embed::RustEmbed)]
99 #[folder = "../runtime/"]
100 #[folder = "../runtime/"]
helix-core/src/textobject.rsnew file+318
@@ -0,0 +1,318 @@
1+use ropey::RopeSlice;
2+
3+use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
4+use crate::movement::{self, Direction};
5+use crate::surround;
6+use crate::Range;
7+
8+fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
9+ this_word_bound_pos(slice, pos, Direction::Forward)
10+}
11+
12+fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
13+ this_word_bound_pos(slice, pos, Direction::Backward)
14+}
15+
16+fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
17+ let iter = match direction {
18+ Direction::Forward => slice.chars_at(pos + 1),
19+ Direction::Backward => {
20+ let mut iter = slice.chars_at(pos);
21+ iter.reverse();
22+ iter
23+ }
24+ };
25+
26+ match categorize_char(slice.char(pos)) {
27+ CharCategory::Eol | CharCategory::Whitespace => pos,
28+ category => {
29+ for peek in iter {
30+ let curr_category = categorize_char(peek);
31+ if curr_category != category
32+ || curr_category == CharCategory::Eol
33+ || curr_category == CharCategory::Whitespace
34+ {
35+ return pos;
36+ }
37+ pos = match direction {
38+ Direction::Forward => pos + 1,
39+ Direction::Backward => pos.saturating_sub(1),
40+ }
41+ }
42+ pos
43+ }
44+ }
45+}
46+
47+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
48+pub enum TextObject {
49+ Around,
50+ Inside,
51+}
52+
53+// count doesn't do anything yet
54+pub fn textobject_word(
55+ slice: RopeSlice,
56+ range: Range,
57+ textobject: TextObject,
58+ count: usize,
59+) -> Range {
60+ let this_word_start = this_word_start_pos(slice, range.head);
61+ let this_word_end = this_word_end_pos(slice, range.head);
62+
63+ let (anchor, head);
64+ match textobject {
65+ TextObject::Inside => {
66+ anchor = this_word_start;
67+ head = this_word_end;
68+ }
69+ TextObject::Around => {
70+ if slice
71+ .get_char(this_word_end + 1)
72+ .map_or(true, char_is_line_ending)
73+ {
74+ head = this_word_end;
75+ if slice
76+ .get_char(this_word_start.saturating_sub(1))
77+ .map_or(true, char_is_line_ending)
78+ {
79+ // single word on a line
80+ anchor = this_word_start;
81+ } else {
82+ // last word on a line, select the whitespace before it too
83+ anchor = movement::move_prev_word_end(slice, range, count).head;
84+ }
85+ } else if char_is_whitespace(slice.char(range.head)) {
86+ // select whole whitespace and next word
87+ head = movement::move_next_word_end(slice, range, count).head;
88+ anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
89+ .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
90+ .unwrap_or(0);
91+ } else {
92+ head = movement::move_next_word_start(slice, range, count).head;
93+ anchor = this_word_start;
94+ }
95+ }
96+ };
97+ Range::new(anchor, head)
98+}
99+
100+pub fn textobject_surround(
101+ slice: RopeSlice,
102+ range: Range,
103+ textobject: TextObject,
104+ ch: char,
105+ count: usize,
106+) -> Range {
107+ surround::find_nth_pairs_pos(slice, ch, range.head, count)
108+ .map(|(anchor, head)| match textobject {
109+ TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
110+ TextObject::Around => Range::new(anchor, head),
111+ })
112+ .unwrap_or(range)
113+}
114+
115+#[cfg(test)]
116+mod test {
117+ use super::TextObject::*;
118+ use super::*;
119+
120+ use crate::Range;
121+ use ropey::Rope;
122+
123+ #[test]
124+ fn test_textobject_word() {
125+ // (text, [(cursor position, textobject, final range), ...])
126+ let tests = &[
127+ (
128+ "cursor at beginning of doc",
129+ vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
130+ ),
131+ (
132+ "cursor at middle of word",
133+ vec![
134+ (13, Inside, (10, 15)),
135+ (10, Inside, (10, 15)),
136+ (15, Inside, (10, 15)),
137+ (13, Around, (10, 16)),
138+ (10, Around, (10, 16)),
139+ (15, Around, (10, 16)),
140+ ],
141+ ),
142+ (
143+ "cursor between word whitespace",
144+ vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
145+ ),
146+ (
147+ "cursor on word before newline\n",
148+ vec![
149+ (22, Inside, (22, 28)),
150+ (28, Inside, (22, 28)),
151+ (25, Inside, (22, 28)),
152+ (22, Around, (21, 28)),
153+ (28, Around, (21, 28)),
154+ (25, Around, (21, 28)),
155+ ],
156+ ),
157+ (
158+ "cursor on newline\nnext line",
159+ vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
160+ ),
161+ (
162+ "cursor on word after newline\nnext line",
163+ vec![
164+ (29, Inside, (29, 32)),
165+ (30, Inside, (29, 32)),
166+ (32, Inside, (29, 32)),
167+ (29, Around, (29, 33)),
168+ (30, Around, (29, 33)),
169+ (32, Around, (29, 33)),
170+ ],
171+ ),
172+ (
173+ "cursor on #$%:;* punctuation",
174+ vec![
175+ (13, Inside, (10, 15)),
176+ (10, Inside, (10, 15)),
177+ (15, Inside, (10, 15)),
178+ (13, Around, (10, 16)),
179+ (10, Around, (10, 16)),
180+ (15, Around, (10, 16)),
181+ ],
182+ ),
183+ (
184+ "cursor on punc%^#$:;.tuation",
185+ vec![
186+ (14, Inside, (14, 20)),
187+ (20, Inside, (14, 20)),
188+ (17, Inside, (14, 20)),
189+ (14, Around, (14, 20)),
190+ // FIXME: edge case
191+ // (20, Around, (14, 20)),
192+ (17, Around, (14, 20)),
193+ ],
194+ ),
195+ (
196+ "cursor in extra whitespace",
197+ vec![
198+ (9, Inside, (9, 9)),
199+ (10, Inside, (10, 10)),
200+ (11, Inside, (11, 11)),
201+ (9, Around, (9, 16)),
202+ (10, Around, (9, 16)),
203+ (11, Around, (9, 16)),
204+ ],
205+ ),
206+ (
207+ "cursor at end of doc",
208+ vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
209+ ),
210+ ];
211+
212+ for (sample, scenario) in tests {
213+ let doc = Rope::from(*sample);
214+ let slice = doc.slice(..);
215+ for &case in scenario {
216+ let (pos, objtype, expected_range) = case;
217+ let result = textobject_word(slice, Range::point(pos), objtype, 1);
218+ assert_eq!(
219+ result,
220+ expected_range.into(),
221+ "\nCase failed: {:?} - {:?}",
222+ sample,
223+ case
224+ );
225+ }
226+ }
227+ }
228+
229+ #[test]
230+ fn test_textobject_surround() {
231+ // (text, [(cursor position, textobject, final range, count), ...])
232+ let tests = &[
233+ (
234+ "simple (single) surround pairs",
235+ vec![
236+ (3, Inside, (3, 3), '(', 1),
237+ (7, Inside, (8, 13), ')', 1),
238+ (10, Inside, (8, 13), '(', 1),
239+ (14, Inside, (8, 13), ')', 1),
240+ (3, Around, (3, 3), '(', 1),
241+ (7, Around, (7, 14), ')', 1),
242+ (10, Around, (7, 14), '(', 1),
243+ (14, Around, (7, 14), ')', 1),
244+ ],
245+ ),
246+ (
247+ "samexx 'single' surround pairs",
248+ vec![
249+ (3, Inside, (3, 3), '\'', 1),
250+ (7, Inside, (8, 13), '\'', 1),
251+ (10, Inside, (8, 13), '\'', 1),
252+ (14, Inside, (8, 13), '\'', 1),
253+ (3, Around, (3, 3), '\'', 1),
254+ (7, Around, (7, 14), '\'', 1),
255+ (10, Around, (7, 14), '\'', 1),
256+ (14, Around, (7, 14), '\'', 1),
257+ ],
258+ ),
259+ (
260+ "(nested (surround (pairs)) 3 levels)",
261+ vec![
262+ (0, Inside, (1, 34), '(', 1),
263+ (6, Inside, (1, 34), ')', 1),
264+ (8, Inside, (9, 24), '(', 1),
265+ (8, Inside, (9, 34), ')', 2),
266+ (20, Inside, (9, 24), '(', 2),
267+ (20, Inside, (1, 34), ')', 3),
268+ (0, Around, (0, 35), '(', 1),
269+ (6, Around, (0, 35), ')', 1),
270+ (8, Around, (8, 25), '(', 1),
271+ (8, Around, (8, 35), ')', 2),
272+ (20, Around, (8, 25), '(', 2),
273+ (20, Around, (0, 35), ')', 3),
274+ ],
275+ ),
276+ (
277+ "(mixed {surround [pair] same} line)",
278+ vec![
279+ (2, Inside, (1, 33), '(', 1),
280+ (9, Inside, (8, 27), '{', 1),
281+ (18, Inside, (18, 21), '[', 1),
282+ (2, Around, (0, 34), '(', 1),
283+ (9, Around, (7, 28), '{', 1),
284+ (18, Around, (17, 22), '[', 1),
285+ ],
286+ ),
287+ (
288+ "(stepped (surround) pairs (should) skip)",
289+ vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
290+ ),
291+ (
292+ "[surround pairs{\non different]\nlines}",
293+ vec![
294+ (7, Inside, (1, 28), '[', 1),
295+ (15, Inside, (16, 35), '{', 1),
296+ (7, Around, (0, 29), '[', 1),
297+ (15, Around, (15, 36), '{', 1),
298+ ],
299+ ),
300+ ];
301+
302+ for (sample, scenario) in tests {
303+ let doc = Rope::from(*sample);
304+ let slice = doc.slice(..);
305+ for &case in scenario {
306+ let (pos, objtype, expected_range, ch, count) = case;
307+ let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
308+ assert_eq!(
309+ result,
310+ expected_range.into(),
311+ "\nCase failed: {:?} - {:?}",
312+ sample,
313+ case
314+ );
315+ }
316+ }
317+ }
318+}
helix-term/src/application.rs+5−1
@@ -160,7 +160,11 @@ impl Application {
160 }
160 }
161 self.render();
161 self.render();
162 }
162 }
163− Some(callback) = self.jobs.next_job() => {
163+ Some(callback) = self.jobs.futures.next() => {
164+ self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
165+ self.render();
166+ }
167+ Some(callback) = self.jobs.wait_futures.next() => {
164 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
168 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
165 self.render();
169 self.render();
166 }
170 }
helix-term/src/commands.rs+322−156
@@ -16,6 +16,7 @@ use helix_core::{
16use helix_view::{
16use helix_view::{
17 document::{IndentStyle, Mode},
17 document::{IndentStyle, Mode},
18 editor::Action,
18 editor::Action,
19+ info::Info,
19 input::KeyEvent,
20 input::KeyEvent,
20 keyboard::KeyCode,
21 keyboard::KeyCode,
21 view::{View, PADDING},
22 view::{View, PADDING},
@@ -38,6 +39,7 @@ use crate::{
38
39
39use crate::job::{self, Job, Jobs};
40use crate::job::{self, Job, Jobs};
40use futures_util::{FutureExt, TryFutureExt};
41use futures_util::{FutureExt, TryFutureExt};
42+use std::collections::HashMap;
41use std::{fmt, future::Future};
43use std::{fmt, future::Future};
42
44
43use std::{
45use std::{
@@ -45,7 +47,7 @@ use std::{
45 path::{Path, PathBuf},
47 path::{Path, PathBuf},
46};
48};
47
49
48−use once_cell::sync::Lazy;
50+use once_cell::sync::{Lazy, OnceCell};
49use serde::de::{self, Deserialize, Deserializer};
51use serde::de::{self, Deserialize, Deserializer};
50
52
51pub struct Context<'a> {
53pub struct Context<'a> {
@@ -74,6 +76,16 @@ impl<'a> Context<'a> {
74 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
76 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
75 }
77 }
76
78
79+ #[inline]
80+ pub fn on_next_key_mode(&mut self, map: HashMap<KeyEvent, fn(&mut Context)>) {
81+ self.on_next_key(move |cx, event| {
82+ cx.editor.autoinfo = None;
83+ if let Some(func) = map.get(&event) {
84+ func(cx);
85+ }
86+ });
87+ }
88+
77 #[inline]
89 #[inline]
78 pub fn callback<T, F>(
90 pub fn callback<T, F>(
79 &mut self,
91 &mut self,
@@ -153,17 +165,12 @@ impl Command {
153 move_char_right,
165 move_char_right,
154 move_line_up,
166 move_line_up,
155 move_line_down,
167 move_line_down,
156− move_line_end,
157− move_line_start,
158− move_first_nonwhitespace,
159 move_next_word_start,
168 move_next_word_start,
160 move_prev_word_start,
169 move_prev_word_start,
161 move_next_word_end,
170 move_next_word_end,
162 move_next_long_word_start,
171 move_next_long_word_start,
163 move_prev_long_word_start,
172 move_prev_long_word_start,
164 move_next_long_word_end,
173 move_next_long_word_end,
165− move_file_start,
166− move_file_end,
167 extend_next_word_start,
174 extend_next_word_start,
168 extend_prev_word_start,
175 extend_prev_word_start,
169 extend_next_word_end,
176 extend_next_word_end,
@@ -175,7 +182,6 @@ impl Command {
175 find_prev_char,
182 find_prev_char,
176 extend_till_prev_char,
183 extend_till_prev_char,
177 extend_prev_char,
184 extend_prev_char,
178− extend_first_nonwhitespace,
179 replace,
185 replace,
180 page_up,
186 page_up,
181 page_down,
187 page_down,
@@ -185,8 +191,6 @@ impl Command {
185 extend_char_right,
191 extend_char_right,
186 extend_line_up,
192 extend_line_up,
187 extend_line_down,
193 extend_line_down,
188− extend_line_end,
189− extend_line_start,
190 select_all,
194 select_all,
191 select_regex,
195 select_regex,
192 split_selection,
196 split_selection,
@@ -196,6 +200,7 @@ impl Command {
196 extend_search_next,
200 extend_search_next,
197 search_selection,
201 search_selection,
198 extend_line,
202 extend_line,
203+ extend_to_line_bounds,
199 delete_selection,
204 delete_selection,
200 change_selection,
205 change_selection,
201 collapse_selection,
206 collapse_selection,
@@ -217,11 +222,17 @@ impl Command {
217 goto_definition,
222 goto_definition,
218 goto_type_definition,
223 goto_type_definition,
219 goto_implementation,
224 goto_implementation,
225+ goto_file_start,
226+ goto_file_end,
220 goto_reference,
227 goto_reference,
221 goto_first_diag,
228 goto_first_diag,
222 goto_last_diag,
229 goto_last_diag,
223 goto_next_diag,
230 goto_next_diag,
224 goto_prev_diag,
231 goto_prev_diag,
232+ goto_line_start,
233+ goto_line_end,
234+ goto_line_end_newline,
235+ goto_first_nonwhitespace,
225 signature_help,
236 signature_help,
226 insert_tab,
237 insert_tab,
227 insert_newline,
238 insert_newline,
@@ -376,7 +387,7 @@ fn move_line_down(cx: &mut Context) {
376 );
387 );
377}
388}
378
389
379−fn move_line_end(cx: &mut Context) {
390+fn goto_line_end(cx: &mut Context) {
380 let (view, doc) = current!(cx.editor);
391 let (view, doc) = current!(cx.editor);
381 doc.set_selection(
392 doc.set_selection(
382 view.id,
393 view.id,
@@ -388,12 +399,33 @@ fn move_line_end(cx: &mut Context) {
388 let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
399 let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
389 let pos = range.head.max(pos).max(text.line_to_char(line));
400 let pos = range.head.max(pos).max(text.line_to_char(line));
390
401
402+ Range::new(
403+ match doc.mode {
404+ Mode::Normal | Mode::Insert => pos,
405+ Mode::Select => range.anchor,
406+ },
407+ pos,
408+ )
409+ }),
410+ );
411+}
412+
413+fn goto_line_end_newline(cx: &mut Context) {
414+ let (view, doc) = current!(cx.editor);
415+
416+ doc.set_selection(
417+ view.id,
418+ doc.selection(view.id).clone().transform(|range| {
419+ let text = doc.text();
420+ let line = text.char_to_line(range.head);
421+
422+ let pos = line_end_char_index(&text.slice(..), line);
391 Range::new(pos, pos)
423 Range::new(pos, pos)
392 }),
424 }),
393 );
425 );
394}
426}
395
427
396−fn move_line_start(cx: &mut Context) {
428+fn goto_line_start(cx: &mut Context) {
397 let (view, doc) = current!(cx.editor);
429 let (view, doc) = current!(cx.editor);
398 doc.set_selection(
430 doc.set_selection(
399 view.id,
431 view.id,
@@ -403,12 +435,18 @@ fn move_line_start(cx: &mut Context) {
403
435
404 // adjust to start of the line
436 // adjust to start of the line
405 let pos = text.line_to_char(line);
437 let pos = text.line_to_char(line);
406− Range::new(pos, pos)
438+ Range::new(
439+ match doc.mode {
440+ Mode::Normal | Mode::Insert => pos,
441+ Mode::Select => range.anchor,
442+ },
443+ pos,
444+ )
407 }),
445 }),
408 );
446 );
409}
447}
410
448
411−fn move_first_nonwhitespace(cx: &mut Context) {
449+fn goto_first_nonwhitespace(cx: &mut Context) {
412 let (view, doc) = current!(cx.editor);
450 let (view, doc) = current!(cx.editor);
413 doc.set_selection(
451 doc.set_selection(
414 view.id,
452 view.id,
@@ -418,7 +456,13 @@ fn move_first_nonwhitespace(cx: &mut Context) {
418
456
419 if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
457 if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
420 let pos = pos + text.line_to_char(line_idx);
458 let pos = pos + text.line_to_char(line_idx);
421− Range::new(pos, pos)
459+ Range::new(
460+ match doc.mode {
461+ Mode::Normal | Mode::Insert => pos,
462+ Mode::Select => range.anchor,
463+ },
464+ pos,
465+ )
422 } else {
466 } else {
423 range
467 range
424 }
468 }
@@ -426,6 +470,37 @@ fn move_first_nonwhitespace(cx: &mut Context) {
426 );
470 );
427}
471}
428
472
473+fn goto_window(cx: &mut Context, align: Align) {
474+ let (view, doc) = current!(cx.editor);
475+
476+ let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref
477+
478+ let last_line = view.last_line(doc);
479+
480+ let line = match align {
481+ Align::Top => (view.first_line + scrolloff),
482+ Align::Center => (view.first_line + (view.area.height as usize / 2)),
483+ Align::Bottom => last_line.saturating_sub(scrolloff),
484+ }
485+ .min(last_line.saturating_sub(scrolloff));
486+
487+ let pos = doc.text().line_to_char(line);
488+
489+ doc.set_selection(view.id, Selection::point(pos));
490+}
491+
492+fn goto_window_top(cx: &mut Context) {
493+ goto_window(cx, Align::Top)
494+}
495+
496+fn goto_window_middle(cx: &mut Context) {
497+ goto_window(cx, Align::Center)
498+}
499+
500+fn goto_window_bottom(cx: &mut Context) {
501+ goto_window(cx, Align::Bottom)
502+}
503+
429// TODO: move vs extend could take an extra type Extend/Move that would
504// TODO: move vs extend could take an extra type Extend/Move that would
430// Range::new(if Move { pos } if Extend { range.anchor }, pos)
505// Range::new(if Move { pos } if Extend { range.anchor }, pos)
431// since these all really do the same thing
506// since these all really do the same thing
@@ -497,13 +572,13 @@ fn move_next_long_word_end(cx: &mut Context) {
497 );
572 );
498}
573}
499
574
500−fn move_file_start(cx: &mut Context) {
575+fn goto_file_start(cx: &mut Context) {
501 push_jump(cx.editor);
576 push_jump(cx.editor);
502 let (view, doc) = current!(cx.editor);
577 let (view, doc) = current!(cx.editor);
503 doc.set_selection(view.id, Selection::point(0));
578 doc.set_selection(view.id, Selection::point(0));
504}
579}
505
580
506−fn move_file_end(cx: &mut Context) {
581+fn goto_file_end(cx: &mut Context) {
507 push_jump(cx.editor);
582 push_jump(cx.editor);
508 let (view, doc) = current!(cx.editor);
583 let (view, doc) = current!(cx.editor);
509 let text = doc.text();
584 let text = doc.text();
@@ -683,24 +758,6 @@ fn extend_prev_char(cx: &mut Context) {
683 )
758 )
684}
759}
685
760
686−fn extend_first_nonwhitespace(cx: &mut Context) {
687− let (view, doc) = current!(cx.editor);
688− doc.set_selection(
689− view.id,
690− doc.selection(view.id).clone().transform(|range| {
691− let text = doc.text();
692− let line_idx = text.char_to_line(range.head);
693−
694− if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
695− let pos = pos + text.line_to_char(line_idx);
696− Range::new(range.anchor, pos)
697− } else {
698− range
699− }
700− }),
701− );
702−}
703−
704fn replace(cx: &mut Context) {
761fn replace(cx: &mut Context) {
705 let mut buf = [0u8; 4]; // To hold utf8 encoded char.
762 let mut buf = [0u8; 4]; // To hold utf8 encoded char.
706
763
@@ -880,38 +937,6 @@ fn extend_line_down(cx: &mut Context) {
880 );
937 );
881}
938}
882
939
883−fn extend_line_end(cx: &mut Context) {
884− let (view, doc) = current!(cx.editor);
885− doc.set_selection(
886− view.id,
887− doc.selection(view.id).clone().transform(|range| {
888− let text = doc.text().slice(..);
889− let line = text.char_to_line(range.head);
890−
891− let pos = line_end_char_index(&text, line);
892− let pos = graphemes::nth_prev_grapheme_boundary(text, pos, 1);
893− let pos = range.head.max(pos).max(text.line_to_char(line));
894−
895− Range::new(range.anchor, pos)
896− }),
897− );
898−}
899−
900−fn extend_line_start(cx: &mut Context) {
901− let (view, doc) = current!(cx.editor);
902− doc.set_selection(
903− view.id,
904− doc.selection(view.id).clone().transform(|range| {
905− let text = doc.text();
906− let line = text.char_to_line(range.head);
907−
908− // adjust to start of the line
909− let pos = text.line_to_char(line);
910− Range::new(range.anchor, pos)
911− }),
912− );
913−}
914−
915fn select_all(cx: &mut Context) {
940fn select_all(cx: &mut Context) {
916 let (view, doc) = current!(cx.editor);
941 let (view, doc) = current!(cx.editor);
917
942
@@ -1055,6 +1080,27 @@ fn extend_line(cx: &mut Context) {
1055 doc.set_selection(view.id, Selection::single(start, end));
1080 doc.set_selection(view.id, Selection::single(start, end));
1056}
1081}
1057
1082
1083+fn extend_to_line_bounds(cx: &mut Context) {
1084+ let (view, doc) = current!(cx.editor);
1085+
1086+ doc.set_selection(
1087+ view.id,
1088+ doc.selection(view.id).clone().transform(|range| {
1089+ let text = doc.text();
1090+ let start = text.line_to_char(text.char_to_line(range.from()));
1091+ let end = text
1092+ .line_to_char(text.char_to_line(range.to()) + 1)
1093+ .saturating_sub(1);
1094+
1095+ if range.anchor < range.head {
1096+ Range::new(start, end)
1097+ } else {
1098+ Range::new(end, start)
1099+ }
1100+ }),
1101+ );
1102+}
1103+
1058fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1104fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1059 let text = doc.text().slice(..);
1105 let text = doc.text().slice(..);
1060 let selection = doc.selection(view_id).clone().min_width_1(text);
1106 let selection = doc.selection(view_id).clone().min_width_1(text);
@@ -1580,6 +1626,24 @@ mod cmd {
1580 }
1626 }
1581 }
1627 }
1582
1628
1629+ /// Sets the [`Document`]'s encoding..
1630+ fn set_encoding(cx: &mut compositor::Context, args: &[&str], _: PromptEvent) {
1631+ let (_, doc) = current!(cx.editor);
1632+ if let Some(label) = args.first() {
1633+ doc.set_encoding(label)
1634+ .unwrap_or_else(|e| cx.editor.set_error(e.to_string()));
1635+ } else {
1636+ let encoding = doc.encoding().name().to_string();
1637+ cx.editor.set_status(encoding)
1638+ }
1639+ }
1640+
1641+ /// Reload the [`Document`] from its source file.
1642+ fn reload(cx: &mut compositor::Context, _args: &[&str], _: PromptEvent) {
1643+ let (view, doc) = current!(cx.editor);
1644+ doc.reload(view.id).unwrap();
1645+ }
1646+
1583 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
1647 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
1584 TypableCommand {
1648 TypableCommand {
1585 name: "quit",
1649 name: "quit",
@@ -1763,6 +1827,20 @@ mod cmd {
1763 fun: show_current_directory,
1827 fun: show_current_directory,
1764 completer: None,
1828 completer: None,
1765 },
1829 },
1830+ TypableCommand {
1831+ name: "encoding",
1832+ alias: None,
1833+ doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
1834+ fun: set_encoding,
1835+ completer: None,
1836+ },
1837+ TypableCommand {
1838+ name: "reload",
1839+ alias: None,
1840+ doc: "Discard changes and reload from the source file.",
1841+ fun: reload,
1842+ completer: None,
1843+ }
1766 ];
1844 ];
1767
1845
1768 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
1846 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -1955,7 +2033,7 @@ fn symbol_picker(cx: &mut Context) {
1955
2033
1956// I inserts at the first nonwhitespace character of each line with a selection
2034// I inserts at the first nonwhitespace character of each line with a selection
1957fn prepend_to_line(cx: &mut Context) {
2035fn prepend_to_line(cx: &mut Context) {
1958− move_first_nonwhitespace(cx);
2036+ goto_first_nonwhitespace(cx);
1959 let doc = doc_mut!(cx.editor);
2037 let doc = doc_mut!(cx.editor);
1960 enter_insert_mode(doc);
2038 enter_insert_mode(doc);
1961}
2039}
@@ -2124,7 +2202,7 @@ fn push_jump(editor: &mut Editor) {
2124 view.jumps.push(jump);
2202 view.jumps.push(jump);
2125}
2203}
2126
2204
2127−fn switch_to_last_accessed_file(cx: &mut Context) {
2205+fn goto_last_accessed_file(cx: &mut Context) {
2128 let alternate_file = view!(cx.editor).last_accessed_doc;
2206 let alternate_file = view!(cx.editor).last_accessed_doc;
2129 if let Some(alt) = alternate_file {
2207 if let Some(alt) = alternate_file {
2130 cx.editor.switch(alt, Action::Replace);
2208 cx.editor.switch(alt, Action::Replace);
@@ -2133,65 +2211,6 @@ fn switch_to_last_accessed_file(cx: &mut Context) {
2133 }
2211 }
2134}
2212}
2135
2213
2136−fn goto_mode(cx: &mut Context) {
2137− if let Some(count) = cx.count {
2138− push_jump(cx.editor);
2139−
2140− let (view, doc) = current!(cx.editor);
2141− let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2));
2142− let pos = doc.text().line_to_char(line_idx);
2143− doc.set_selection(view.id, Selection::point(pos));
2144− return;
2145− }
2146−
2147− cx.on_next_key(move |cx, event| {
2148− if let KeyEvent {
2149− code: KeyCode::Char(ch),
2150− ..
2151− } = event
2152− {
2153− // TODO: temporarily show GOTO in the mode list
2154− let doc = doc_mut!(cx.editor);
2155− match (doc.mode, ch) {
2156− (_, 'g') => move_file_start(cx),
2157− (_, 'e') => move_file_end(cx),
2158− (_, 'a') => switch_to_last_accessed_file(cx),
2159− (Mode::Normal, 'h') => move_line_start(cx),
2160− (Mode::Normal, 'l') => move_line_end(cx),
2161− (Mode::Select, 'h') => extend_line_start(cx),
2162− (Mode::Select, 'l') => extend_line_end(cx),
2163− (_, 'd') => goto_definition(cx),
2164− (_, 'y') => goto_type_definition(cx),
2165− (_, 'r') => goto_reference(cx),
2166− (_, 'i') => goto_implementation(cx),
2167− (Mode::Normal, 's') => move_first_nonwhitespace(cx),
2168− (Mode::Select, 's') => extend_first_nonwhitespace(cx),
2169−
2170− (_, 't') | (_, 'm') | (_, 'b') => {
2171− let (view, doc) = current!(cx.editor);
2172−
2173− let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref
2174−
2175− let last_line = view.last_line(doc);
2176−
2177− let line = match ch {
2178− 't' => (view.first_line + scrolloff),
2179− 'm' => (view.first_line + (view.area.height as usize / 2)),
2180− 'b' => last_line.saturating_sub(scrolloff),
2181− _ => unreachable!(),
2182− }
2183− .min(last_line.saturating_sub(scrolloff));
2184−
2185− let pos = doc.text().line_to_char(line);
2186−
2187− doc.set_selection(view.id, Selection::point(pos));
2188− }
2189− _ => (),
2190− }
2191− }
2192− })
2193−}
2194−
2195fn select_mode(cx: &mut Context) {
2214fn select_mode(cx: &mut Context) {
2196 let (view, doc) = current!(cx.editor);
2215 let (view, doc) = current!(cx.editor);
2197
2216
@@ -2211,13 +2230,27 @@ fn select_mode(cx: &mut Context) {
2211 }),
2230 }),
2212 );
2231 );
2213
2232
2214− doc.mode = Mode::Select;
2233+ doc_mut!(cx.editor).mode = Mode::Select;
2215}
2234}
2216
2235
2217fn exit_select_mode(cx: &mut Context) {
2236fn exit_select_mode(cx: &mut Context) {
2218 doc_mut!(cx.editor).mode = Mode::Normal;
2237 doc_mut!(cx.editor).mode = Mode::Normal;
2219}
2238}
2220
2239
2240+fn goto_prehook(cx: &mut Context) -> bool {
2241+ if let Some(count) = cx.count {
2242+ push_jump(cx.editor);
2243+
2244+ let (view, doc) = current!(cx.editor);
2245+ let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(1));
2246+ let pos = doc.text().line_to_char(line_idx);
2247+ doc.set_selection(view.id, Selection::point(pos));
2248+ true
2249+ } else {
2250+ false
2251+ }
2252+}
2253+
2221fn goto_impl(
2254fn goto_impl(
2222 editor: &mut Editor,
2255 editor: &mut Editor,
2223 compositor: &mut Compositor,
2256 compositor: &mut Compositor,
@@ -3457,33 +3490,6 @@ fn select_register(cx: &mut Context) {
3457 })
3490 })
3458}
3491}
3459
3492
3460−fn space_mode(cx: &mut Context) {
3461− cx.on_next_key(move |cx, event| {
3462− if let KeyEvent {
3463− code: KeyCode::Char(ch),
3464− ..
3465− } = event
3466− {
3467− // TODO: temporarily show SPC in the mode list
3468− match ch {
3469− 'f' => file_picker(cx),
3470− 'b' => buffer_picker(cx),
3471− 's' => symbol_picker(cx),
3472− 'w' => window_mode(cx),
3473− 'y' => yank_joined_to_clipboard(cx),
3474− 'Y' => yank_main_selection_to_clipboard(cx),
3475− 'p' => paste_clipboard_after(cx),
3476− 'P' => paste_clipboard_before(cx),
3477− 'R' => replace_selections_with_clipboard(cx),
3478− // ' ' => toggle_alternate_buffer(cx),
3479− // TODO: temporary since space mode took its old key
3480− ' ' => keep_primary_selection(cx),
3481− _ => (),
3482− }
3483− }
3484− })
3485−}
3486−
3487fn view_mode(cx: &mut Context) {
3493fn view_mode(cx: &mut Context) {
3488 cx.on_next_key(move |cx, event| {
3494 cx.on_next_key(move |cx, event| {
3489 if let KeyEvent {
3495 if let KeyEvent {
@@ -3559,6 +3565,9 @@ fn right_bracket_mode(cx: &mut Context) {
3559 })
3565 })
3560}
3566}
3561
3567
3568+use helix_core::surround;
3569+use helix_core::textobject;
3570+
3562fn match_mode(cx: &mut Context) {
3571fn match_mode(cx: &mut Context) {
3563 let count = cx.count;
3572 let count = cx.count;
3564 cx.on_next_key(move |cx, event| {
3573 cx.on_next_key(move |cx, event| {
@@ -3574,13 +3583,41 @@ fn match_mode(cx: &mut Context) {
3574 's' => surround_add(cx),
3583 's' => surround_add(cx),
3575 'r' => surround_replace(cx),
3584 'r' => surround_replace(cx),
3576 'd' => surround_delete(cx),
3585 'd' => surround_delete(cx),
3586+ 'a' => select_textobject(cx, textobject::TextObject::Around),
3587+ 'i' => select_textobject(cx, textobject::TextObject::Inside),
3577 _ => (),
3588 _ => (),
3578 }
3589 }
3579 }
3590 }
3580 })
3591 })
3581}
3592}
3582
3593
3583−use helix_core::surround;
3594+fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
3595+ let count = cx.count();
3596+ cx.on_next_key(move |cx, event| {
3597+ if let KeyEvent {
3598+ code: KeyCode::Char(ch),
3599+ ..
3600+ } = event
3601+ {
3602+ let (view, doc) = current!(cx.editor);
3603+
3604+ doc.set_selection(
3605+ view.id,
3606+ doc.selection(view.id).clone().transform(|range| {
3607+ let text = doc.text().slice(..);
3608+ match ch {
3609+ 'w' => textobject::textobject_word(text, range, objtype, count),
3610+ // TODO: cancel new ranges if inconsistent surround matches across lines
3611+ ch if !ch.is_ascii_alphanumeric() => {
3612+ textobject::textobject_surround(text, range, objtype, ch, count)
3613+ }
3614+ _ => range,
3615+ }
3616+ }),
3617+ );
3618+ }
3619+ })
3620+}
3584
3621
3585fn surround_add(cx: &mut Context) {
3622fn surround_add(cx: &mut Context) {
3586 cx.on_next_key(move |cx, event| {
3623 cx.on_next_key(move |cx, event| {
@@ -3671,3 +3708,132 @@ fn surround_delete(cx: &mut Context) {
3671 }
3708 }
3672 })
3709 })
3673}
3710}
3711+
3712+/// Do nothing, just for modeinfo.
3713+fn noop(_cx: &mut Context) -> bool {
3714+ false
3715+}
3716+
3717+/// Generate modeinfo.
3718+///
3719+/// If prehook returns true then it will stop the rest.
3720+macro_rules! mode_info {
3721+ // TODO: reuse $mode for $stat
3722+ (@join $first:expr $(,$rest:expr)*) => {
3723+ concat!($first, $(", ", $rest),*)
3724+ };
3725+ (@name #[doc = $name:literal] $(#[$rest:meta])*) => {
3726+ $name
3727+ };
3728+ {
3729+ #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident,
3730+ $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
3731+ } => {
3732+ mode_info! {
3733+ #[doc = $name]
3734+ $(#[$doc])*
3735+ $mode, $stat, noop,
3736+ $(
3737+ #[doc = $desc]
3738+ $($key)|+ => $func
3739+ ),+,
3740+ }
3741+ };
3742+ {
3743+ #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr,
3744+ $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
3745+ } => {
3746+ #[doc = $name]
3747+ $(#[$doc])*
3748+ #[doc = ""]
3749+ #[doc = "<table><tr><th>key</th><th>desc</th></tr><tbody>"]
3750+ $(
3751+ #[doc = "<tr><td>"]
3752+ // TODO switch to this once we use rust 1.54
3753+ // right now it will produce multiple rows
3754+ // #[doc = mode_info!(@join $($key),+)]
3755+ $(
3756+ #[doc = $key]
3757+ )+
3758+ // <-
3759+ #[doc = "</td><td>"]
3760+ #[doc = $desc]
3761+ #[doc = "</td></tr>"]
3762+ )+
3763+ #[doc = "</tbody></table>"]
3764+ pub fn $mode(cx: &mut Context) {
3765+ if $prehook(cx) {
3766+ return;
3767+ }
3768+ static $stat: OnceCell<Info> = OnceCell::new();
3769+ cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key(
3770+ $name.trim(),
3771+ vec![$((&[$($key.parse().unwrap()),+], $desc)),+],
3772+ )));
3773+ use helix_core::hashmap;
3774+ // TODO: try and convert this to match later
3775+ let map = hashmap! {
3776+ $($($key.parse::<KeyEvent>().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),*
3777+ };
3778+ cx.on_next_key_mode(map);
3779+ }
3780+ };
3781+}
3782+
3783+mode_info! {
3784+ /// space mode
3785+ space_mode, SPACE_MODE,
3786+ /// file picker
3787+ "f" => file_picker,
3788+ /// buffer picker
3789+ "b" => buffer_picker,
3790+ /// symbol picker
3791+ "s" => symbol_picker,
3792+ /// window mode
3793+ "w" => window_mode,
3794+ /// yank joined to clipboard
3795+ "y" => yank_joined_to_clipboard,
3796+ /// yank main selection to clipboard
3797+ "Y" => yank_main_selection_to_clipboard,
3798+ /// paste system clipboard after selections
3799+ "p" => paste_clipboard_after,
3800+ /// paste system clipboard before selections
3801+ "P" => paste_clipboard_before,
3802+ /// replace selections with clipboard
3803+ "R" => replace_selections_with_clipboard,
3804+ /// keep primary selection
3805+ "space" => keep_primary_selection,
3806+}
3807+
3808+mode_info! {
3809+ /// goto mode
3810+ ///
3811+ /// When specified with a count, it will go to that line without entering the mode.
3812+ goto_mode, GOTO_MODE, goto_prehook,
3813+ /// file start
3814+ "g" => goto_file_start,
3815+ /// file end
3816+ "e" => goto_file_end,
3817+ /// line start
3818+ "h" => goto_line_start,
3819+ /// line end
3820+ "l" => goto_line_end,
3821+ /// line first non blank
3822+ "s" => goto_first_nonwhitespace,
3823+ /// definition
3824+ "d" => goto_definition,
3825+ /// type references
3826+ "y" => goto_type_definition,
3827+ /// references
3828+ "r" => goto_reference,
3829+ /// implementation
3830+ "i" => goto_implementation,
3831+ /// window top
3832+ "t" => goto_window_top,
3833+ /// window middle
3834+ "m" => goto_window_middle,
3835+ /// window bottom
3836+ "b" => goto_window_bottom,
3837+ /// last accessed file
3838+ "a" => goto_last_accessed_file,
3839+}
helix-term/src/job.rs+7−7
@@ -16,9 +16,9 @@ pub struct Job {
16
16
17#[derive(Default)]
17#[derive(Default)]
18pub struct Jobs {
18pub struct Jobs {
19− futures: FuturesUnordered<JobFuture>,
19+ pub futures: FuturesUnordered<JobFuture>,
20 /// These are the ones that need to complete before we exit.
20 /// These are the ones that need to complete before we exit.
21− wait_futures: FuturesUnordered<JobFuture>,
21+ pub wait_futures: FuturesUnordered<JobFuture>,
22}
22}
23
23
24impl Job {
24impl Job {
@@ -77,11 +77,11 @@ impl Jobs {
77 }
77 }
78 }
78 }
79
79
80− pub fn next_job(
81− &mut self,
82− ) -> impl Future<Output = Option<anyhow::Result<Option<Callback>>>> + '_ {
80+ pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
81+ tokio::select! {
82+ event = self.futures.next() => { event }
83− future::select(self.futures.next(), self.wait_futures.next())
83+ event = self.wait_futures.next() => { event }
84− .map(|either| either.factor_first().0)
84+ }
85 }
85 }
86
86
87 pub fn add(&mut self, j: Job) {
87 pub fn add(&mut self, j: Job) {
helix-term/src/keymap.rs+19−109
@@ -1,118 +1,25 @@
1pub use crate::commands::Command;
1pub use crate::commands::Command;
2use crate::config::Config;
2use crate::config::Config;
3use helix_core::hashmap;
3use helix_core::hashmap;
4−use helix_view::{
4+use helix_view::{document::Mode, input::KeyEvent};
5− document::Mode,
6− input::KeyEvent,
7− keyboard::{KeyCode, KeyModifiers},
8−};
9use serde::Deserialize;
5use serde::Deserialize;
10use std::{
6use std::{
11 collections::HashMap,
7 collections::HashMap,
12 ops::{Deref, DerefMut},
8 ops::{Deref, DerefMut},
13};
9};
14
10
15−// Kakoune-inspired:
16−// mode = {
17−// normal = {
18−// q = record_macro
19−// w = (next) word
20−// W = next WORD
21−// e = end of word
22−// E = end of WORD
23−// r = replace
24−// R = replace with yanked
25−// t = 'till char
26−// y = yank
27−// u = undo
28−// U = redo
29−// i = insert
30−// I = INSERT (start of line)
31−// o = open below (insert on new line below)
32−// O = open above (insert on new line above)
33−// p = paste (before cursor)
34−// P = PASTE (after cursor)
35−// ` =
36−// [ = select to text object start (alt = select whole object)
37−// ] = select to text object end
38−// { = extend to inner object start
39−// } = extend to inner object end
40−// a = append
41−// A = APPEND (end of line)
42−// s = split
43−// S = select
44−// d = delete()
45−// f = find_char()
46−// g = goto (gg, G, gc, gd, etc)
47−//
48−// h = move_char_left(n) || arrow-left = move_char_left(n)
49−// j = move_line_down(n) || arrow-down = move_line_down(n)
50−// k = move_line_up(n) || arrow_up = move_line_up(n)
51−// l = move_char_right(n) || arrow-right = move_char_right(n)
52−// : = command line
53−// ; = collapse selection to cursor
54−// " = use register
55−// ` = convert case? (to lower) (alt = swap case)
56−// ~ = convert to upper case
57−// . = repeat last command
58−// \ = disable hook?
59−// / = search
60−// > = indent
61−// < = deindent
62−// % = select whole buffer (in vim = jump to matching bracket)
63−// * = search pattern in selection
64−// ( = rotate main selection backward
65−// ) = rotate main selection forward
66−// - = trim selections? (alt = merge contiguous sel together)
67−// @ = convert tabs to spaces
68−// & = align cursor
69−// ? = extend to next given regex match (alt = to prev)
70−//
71−// in kakoune these are alt-h alt-l / gh gl
72−// select from curs to begin end / move curs to begin end
73−// 0 = start of line
74−// ^ = start of line(first non blank char) || Home = start of line(first non blank char)
75−// $ = end of line || End = end of line
76−//
77−// z = save selections
78−// Z = restore selections
79−// x = select line
80−// X = extend line
81−// c = change selected text
82−// C = copy selection?
83−// v = view menu (viewport manipulation)
84−// b = select to previous word start
85−// B = select to previous WORD start
86−//
87−//
88−//
89−//
90−//
91−//
92−// = = align?
93−// + =
94−// }
95−//
96−// gd = goto definition
97−// gr = goto reference
98−// [d = previous diagnostic
99−// d] = next diagnostic
100−// [D = first diagnostic
101−// D] = last diagnostic
102−// }
103−
104#[macro_export]
11#[macro_export]
105macro_rules! key {
12macro_rules! key {
106 ($key:ident) => {
13 ($key:ident) => {
107 KeyEvent {
14 KeyEvent {
108− code: KeyCode::$key,
15+ code: ::helix_view::keyboard::KeyCode::$key,
109− modifiers: KeyModifiers::NONE,
16+ modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
110 }
17 }
111 };
18 };
112 ($($ch:tt)*) => {
19 ($($ch:tt)*) => {
113 KeyEvent {
20 KeyEvent {
114− code: KeyCode::Char($($ch)*),
21+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
115− modifiers: KeyModifiers::NONE,
22+ modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
116 }
23 }
117 };
24 };
118}
25}
@@ -120,8 +27,8 @@ macro_rules! key {
120macro_rules! ctrl {
27macro_rules! ctrl {
121 ($($ch:tt)*) => {
28 ($($ch:tt)*) => {
122 KeyEvent {
29 KeyEvent {
123− code: KeyCode::Char($($ch)*),
30+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
124− modifiers: KeyModifiers::CONTROL,
31+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
125 }
32 }
126 };
33 };
127}
34}
@@ -129,8 +36,8 @@ macro_rules! ctrl {
129macro_rules! alt {
36macro_rules! alt {
130 ($($ch:tt)*) => {
37 ($($ch:tt)*) => {
131 KeyEvent {
38 KeyEvent {
132− code: KeyCode::Char($($ch)*),
39+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
133− modifiers: KeyModifiers::ALT,
40+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
134 }
41 }
135 };
42 };
136}
43}
@@ -175,8 +82,8 @@ impl Default for Keymaps {
175 key!('r') => Command::replace,
82 key!('r') => Command::replace,
176 key!('R') => Command::replace_with_yanked,
83 key!('R') => Command::replace_with_yanked,
177
84
178− key!(Home) => Command::move_line_start,
85+ key!(Home) => Command::goto_line_start,
179− key!(End) => Command::move_line_end,
86+ key!(End) => Command::goto_line_end,
180
87
181 key!('w') => Command::move_next_word_start,
88 key!('w') => Command::move_next_word_start,
182 key!('b') => Command::move_prev_word_start,
89 key!('b') => Command::move_prev_word_start,
@@ -213,7 +120,9 @@ impl Default for Keymaps {
213 alt!(';') => Command::flip_selections,
120 alt!(';') => Command::flip_selections,
214 key!('%') => Command::select_all,
121 key!('%') => Command::select_all,
215 key!('x') => Command::extend_line,
122 key!('x') => Command::extend_line,
123+ key!('x') => Command::extend_line,
124+ key!('X') => Command::extend_to_line_bounds,
216− // extend_to_whole_line, crop_to_whole_line
125+ // crop_to_whole_line
217
126
218
127
219 key!('m') => Command::match_mode,
128 key!('m') => Command::match_mode,
@@ -307,8 +216,8 @@ impl Default for Keymaps {
307
216
308 key!('T') => Command::extend_till_prev_char,
217 key!('T') => Command::extend_till_prev_char,
309 key!('F') => Command::extend_prev_char,
218 key!('F') => Command::extend_prev_char,
310− key!(Home) => Command::extend_line_start,
219+ key!(Home) => Command::goto_line_start,
311− key!(End) => Command::extend_line_end,
220+ key!(End) => Command::goto_line_end,
312 key!(Esc) => Command::exit_select_mode,
221 key!(Esc) => Command::exit_select_mode,
313 )
222 )
314 .into_iter(),
223 .into_iter(),
@@ -331,8 +240,8 @@ impl Default for Keymaps {
331 key!(Right) => Command::move_char_right,
240 key!(Right) => Command::move_char_right,
332 key!(PageUp) => Command::page_up,
241 key!(PageUp) => Command::page_up,
333 key!(PageDown) => Command::page_down,
242 key!(PageDown) => Command::page_down,
334− key!(Home) => Command::move_line_start,
243+ key!(Home) => Command::goto_line_start,
335− key!(End) => Command::move_line_end,
244+ key!(End) => Command::goto_line_end_newline,
336 ctrl!('x') => Command::completion,
245 ctrl!('x') => Command::completion,
337 ctrl!('w') => Command::delete_word_backward,
246 ctrl!('w') => Command::delete_word_backward,
338 ),
247 ),
@@ -352,6 +261,7 @@ pub fn merge_keys(mut config: Config) -> Config {
352
261
353#[test]
262#[test]
354fn merge_partial_keys() {
263fn merge_partial_keys() {
264+ use helix_view::keyboard::{KeyCode, KeyModifiers};
355 let config = Config {
265 let config = Config {
356 keys: Keymaps(hashmap! {
266 keys: Keymaps(hashmap! {
357 Mode::Normal => hashmap! {
267 Mode::Normal => hashmap! {
helix-term/src/ui/editor.rs+6−2
@@ -738,6 +738,11 @@ impl Component for EditorView {
738 self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused);
738 self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused);
739 }
739 }
740
740
741+ if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) {
742+ info.render(area, surface, cx);
743+ cx.editor.autoinfo = Some(info);
744+ }
745+
741 // render status msg
746 // render status msg
742 if let Some((status_msg, severity)) = &cx.editor.status_msg {
747 if let Some((status_msg, severity)) = &cx.editor.status_msg {
743 use helix_view::editor::Severity;
748 use helix_view::editor::Severity;
@@ -756,8 +761,7 @@ impl Component for EditorView {
756 }
761 }
757
762
758 if let Some(completion) = &self.completion {
763 if let Some(completion) = &self.completion {
759− completion.render(area, surface, cx)
764+ completion.render(area, surface, cx);
760− // render completion here
761 }
765 }
762 }
766 }
763
767
helix-term/src/ui/info.rsnew file+30
@@ -0,0 +1,30 @@
1+use crate::compositor::{Component, Context};
2+use helix_view::graphics::Rect;
3+use helix_view::info::Info;
4+use tui::buffer::Buffer as Surface;
5+use tui::widgets::{Block, Borders, Widget};
6+
7+impl Component for Info {
8+ fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
9+ let style = cx.editor.theme.get("ui.popup");
10+ let block = Block::default()
11+ .title(self.title)
12+ .borders(Borders::ALL)
13+ .border_style(style);
14+ let Info { width, height, .. } = self;
15+ let (w, h) = (*width + 2, *height + 2);
16+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
17+ let area = viewport.intersection(Rect::new(
18+ viewport.width.saturating_sub(w),
19+ viewport.height.saturating_sub(h + 2),
20+ w,
21+ h,
22+ ));
23+ surface.clear_with(area, style);
24+ let Rect { x, y, .. } = block.inner(area);
25+ for (y, line) in (y..).zip(self.text.lines()) {
26+ surface.set_string(x, y, line, style);
27+ }
28+ block.render(area, surface);
29+ }
30+}
helix-term/src/ui/mod.rs+1
@@ -1,5 +1,6 @@
1mod completion;
1mod completion;
2mod editor;
2mod editor;
3+mod info;
3mod markdown;
4mod markdown;
4mod menu;
5mod menu;
5mod picker;
6mod picker;
helix-tui/Cargo.toml−1
@@ -19,7 +19,6 @@ default = ["crossterm"]
19bitflags = "1.0"
19bitflags = "1.0"
20cassowary = "0.3"
20cassowary = "0.3"
21unicode-segmentation = "1.2"
21unicode-segmentation = "1.2"
22−unicode-width = "0.1"
23crossterm = { version = "0.20", optional = true }
22crossterm = { version = "0.20", optional = true }
24serde = { version = "1", "optional" = true, features = ["derive"]}
23serde = { version = "1", "optional" = true, features = ["derive"]}
25helix-view = { version = "0.3", path = "../helix-view", features = ["term"] }
24helix-view = { version = "0.3", path = "../helix-view", features = ["term"] }
helix-tui/src/backend/test.rs+1−1
@@ -2,9 +2,9 @@ use crate::{
2 backend::Backend,
2 backend::Backend,
3 buffer::{Buffer, Cell},
3 buffer::{Buffer, Cell},
4};
4};
5+use helix_core::unicode::width::UnicodeWidthStr;
5use helix_view::graphics::{CursorKind, Rect};
6use helix_view::graphics::{CursorKind, Rect};
6use std::{fmt::Write, io};
7use std::{fmt::Write, io};
7−use unicode_width::UnicodeWidthStr;
8
8
9/// A backend used for the integration tests.
9/// A backend used for the integration tests.
10#[derive(Debug)]
10#[derive(Debug)]
helix-tui/src/buffer.rs+1−1
@@ -1,7 +1,7 @@
1use crate::text::{Span, Spans};
1use crate::text::{Span, Spans};
2+use helix_core::unicode::width::UnicodeWidthStr;
2use std::cmp::min;
3use std::cmp::min;
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_segmentation::UnicodeSegmentation;
4−use unicode_width::UnicodeWidthStr;
5
5
6use helix_view::graphics::{Color, Modifier, Rect, Style};
6use helix_view::graphics::{Color, Modifier, Rect, Style};
7
7
helix-tui/src/text.rs+1−1
@@ -47,10 +47,10 @@
47//! ]);
47//! ]);
48//! ```
48//! ```
49use helix_core::line_ending::str_is_line_ending;
49use helix_core::line_ending::str_is_line_ending;
50+use helix_core::unicode::width::UnicodeWidthStr;
50use helix_view::graphics::Style;
51use helix_view::graphics::Style;
51use std::borrow::Cow;
52use std::borrow::Cow;
52use unicode_segmentation::UnicodeSegmentation;
53use unicode_segmentation::UnicodeSegmentation;
53−use unicode_width::UnicodeWidthStr;
54
54
55/// A grapheme associated to a style.
55/// A grapheme associated to a style.
56#[derive(Debug, Clone, PartialEq)]
56#[derive(Debug, Clone, PartialEq)]
helix-tui/src/widgets/paragraph.rs+1−1
@@ -7,9 +7,9 @@ use crate::{
7 Block, Widget,
7 Block, Widget,
8 },
8 },
9};
9};
10+use helix_core::unicode::width::UnicodeWidthStr;
10use helix_view::graphics::{Rect, Style};
11use helix_view::graphics::{Rect, Style};
11use std::iter;
12use std::iter;
12−use unicode_width::UnicodeWidthStr;
13
13
14fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
14fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
15 match alignment {
15 match alignment {
helix-tui/src/widgets/reflow.rs+1−1
@@ -1,7 +1,7 @@
1use crate::text::StyledGrapheme;
1use crate::text::StyledGrapheme;
2use helix_core::line_ending::str_is_line_ending;
2use helix_core::line_ending::str_is_line_ending;
3+use helix_core::unicode::width::UnicodeWidthStr;
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_segmentation::UnicodeSegmentation;
4−use unicode_width::UnicodeWidthStr;
5
5
6const NBSP: &str = "\u{00a0}";
6const NBSP: &str = "\u{00a0}";
7
7
helix-tui/src/widgets/table.rs+1−1
@@ -9,9 +9,9 @@ use cassowary::{
9 WeightedRelation::*,
9 WeightedRelation::*,
10 {Expression, Solver},
10 {Expression, Solver},
11};
11};
12+use helix_core::unicode::width::UnicodeWidthStr;
12use helix_view::graphics::{Rect, Style};
13use helix_view::graphics::{Rect, Style};
13use std::collections::HashMap;
14use std::collections::HashMap;
14−use unicode_width::UnicodeWidthStr;
15
15
16/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
16/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
17///
17///
helix-view/src/document.rs+67−17
@@ -70,7 +70,6 @@ pub enum IndentStyle {
70}
70}
71
71
72pub struct Document {
72pub struct Document {
73− // rope + selection
74 pub(crate) id: DocumentId,
73 pub(crate) id: DocumentId,
75 text: Rope,
74 text: Rope,
76 pub(crate) selections: HashMap<ViewId, Selection>,
75 pub(crate) selections: HashMap<ViewId, Selection>,
@@ -307,6 +306,19 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
307 Ok(())
306 Ok(())
308}
307}
309
308
309+/// 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)
310+pub fn with_line_ending(rope: &mut Rope) -> LineEnding {
311+ // search for line endings
312+ let line_ending = auto_detect_line_ending(rope).unwrap_or(DEFAULT_LINE_ENDING);
313+
314+ // add missing newline at the end of file
315+ if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
316+ rope.insert(rope.len_chars(), line_ending.as_str());
317+ }
318+
319+ line_ending
320+}
321+
310/// Like std::mem::replace() except it allows the replacement value to be mapped from the
322/// Like std::mem::replace() except it allows the replacement value to be mapped from the
311/// original value.
323/// original value.
312fn take_with<T, F>(mut_ref: &mut T, closure: F)
324fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -395,12 +407,13 @@ pub fn normalize_path(path: &Path) -> PathBuf {
395/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
407/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
396/// here if the path exists, just normalize it's components.
408/// here if the path exists, just normalize it's components.
397pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
409pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
398− let normalized = normalize_path(path);
410+ let path = if path.is_relative() {
399− if normalized.is_absolute() {
411+ std::env::current_dir().map(|current_dir| current_dir.join(path))?
400− Ok(normalized)
401 } else {
412 } else {
402− std::env::current_dir().map(|current_dir| current_dir.join(normalized))
413+ path.to_path_buf()
403− }
414+ };
415+
416+ Ok(normalize_path(&path))
404}
417}
405
418
406use helix_lsp::lsp;
419use helix_lsp::lsp;
@@ -448,7 +461,8 @@ impl Document {
448 }
461 }
449
462
450 let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
463 let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
451− let (rope, encoding) = from_reader(&mut file, encoding)?;
464+ let (mut rope, encoding) = from_reader(&mut file, encoding)?;
465+ let line_ending = with_line_ending(&mut rope);
452
466
453 let mut doc = Self::from(rope, Some(encoding));
467 let mut doc = Self::from(rope, Some(encoding));
454
468
@@ -458,9 +472,9 @@ impl Document {
458 doc.detect_language(theme, loader);
472 doc.detect_language(theme, loader);
459 }
473 }
460
474
461− // Detect indentation style and line ending.
475+ // Detect indentation style and set line ending.
462 doc.detect_indent_style();
476 doc.detect_indent_style();
463− doc.line_ending = auto_detect_line_ending(&doc.text).unwrap_or(DEFAULT_LINE_ENDING);
477+ doc.line_ending = line_ending;
464
478
465 Ok(doc)
479 Ok(doc)
466 }
480 }
@@ -578,6 +592,45 @@ impl Document {
578 }
592 }
579 }
593 }
580
594
595+ /// Reload the document from its path.
596+ pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> {
597+ let encoding = &self.encoding;
598+ let path = self.path().filter(|path| path.exists());
599+
600+ // If there is no path or the path no longer exists.
601+ if path.is_none() {
602+ return Err(anyhow!("can't find file to reload from"));
603+ }
604+
605+ let mut file = std::fs::File::open(path.unwrap())?;
606+ let (mut rope, ..) = from_reader(&mut file, Some(encoding))?;
607+ let line_ending = with_line_ending(&mut rope);
608+
609+ let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
610+ self.apply(&transaction, view_id);
611+ self.append_changes_to_history(view_id);
612+
613+ // Detect indentation style and set line ending.
614+ self.detect_indent_style();
615+ self.line_ending = line_ending;
616+
617+ Ok(())
618+ }
619+
620+ /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
621+ pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
622+ match encoding_rs::Encoding::for_label(label.as_bytes()) {
623+ Some(encoding) => self.encoding = encoding,
624+ None => return Err(anyhow::anyhow!("unknown encoding")),
625+ }
626+ Ok(())
627+ }
628+
629+ /// Returns the [`Document`]'s current encoding.
630+ pub fn encoding(&self) -> &'static encoding_rs::Encoding {
631+ self.encoding
632+ }
633+
581 fn detect_indent_style(&mut self) {
634 fn detect_indent_style(&mut self) {
582 // Build a histogram of the indentation *increases* between
635 // Build a histogram of the indentation *increases* between
583 // subsequent lines, ignoring lines that are all whitespace.
636 // subsequent lines, ignoring lines that are all whitespace.
@@ -996,14 +1049,11 @@ impl Document {
996 let cwdir = std::env::current_dir().expect("couldn't determine current directory");
1049 let cwdir = std::env::current_dir().expect("couldn't determine current directory");
997
1050
998 self.path.as_ref().map(|path| {
1051 self.path.as_ref().map(|path| {
999− let path = fold_home_dir(path);
1052+ let mut path = path.as_path();
1000− if path.is_relative() {
1053+ if path.is_absolute() {
1001− path
1002− } else {
1003− path.strip_prefix(cwdir)
1054+ path = path.strip_prefix(cwdir).unwrap_or(path)
1004− .map(|p| p.to_path_buf())
1005− .unwrap_or(path)
1006− }
1055+ };
1056+ fold_home_dir(path)
1007 })
1057 })
1008 }
1058 }
1009
1059
helix-view/src/editor.rs+3
@@ -1,6 +1,7 @@
1use crate::{
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 graphics::{CursorKind, Rect},
3 graphics::{CursorKind, Rect},
4+ info::Info,
4 theme::{self, Theme},
5 theme::{self, Theme},
5 tree::Tree,
6 tree::Tree,
6 Document, DocumentId, RegisterSelection, View, ViewId,
7 Document, DocumentId, RegisterSelection, View, ViewId,
@@ -32,6 +33,7 @@ pub struct Editor {
32 pub syn_loader: Arc<syntax::Loader>,
33 pub syn_loader: Arc<syntax::Loader>,
33 pub theme_loader: Arc<theme::Loader>,
34 pub theme_loader: Arc<theme::Loader>,
34
35
36+ pub autoinfo: Option<&'static Info>,
35 pub status_msg: Option<(String, Severity)>,
37 pub status_msg: Option<(String, Severity)>,
36}
38}
37
39
@@ -64,6 +66,7 @@ impl Editor {
64 theme_loader: themes,
66 theme_loader: themes,
65 registers: Registers::default(),
67 registers: Registers::default(),
66 clipboard_provider: get_clipboard_provider(),
68 clipboard_provider: get_clipboard_provider(),
69+ autoinfo: None,
67 status_msg: None,
70 status_msg: None,
68 }
71 }
69 }
72 }
helix-view/src/info.rsnew file+57
@@ -0,0 +1,57 @@
1+use crate::input::KeyEvent;
2+use helix_core::unicode::width::UnicodeWidthStr;
3+use std::fmt::Write;
4+
5+#[derive(Debug)]
6+/// Info box used in editor. Rendering logic will be in other crate.
7+pub struct Info {
8+ /// Title kept as static str for now.
9+ pub title: &'static str,
10+ /// Text body, should contains newline.
11+ pub text: String,
12+ /// Body width.
13+ pub width: u16,
14+ /// Body height.
15+ pub height: u16,
16+}
17+
18+impl Info {
19+ pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info {
20+ let (lpad, mpad, rpad) = (1, 2, 1);
21+ let keymaps_width: u16 = body
22+ .iter()
23+ .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
24+ .max()
25+ .unwrap();
26+ let mut text = String::new();
27+ let mut width = 0;
28+ let height = body.len() as u16;
29+ for (keyevents, desc) in body {
30+ let keyevent = keyevents[0];
31+ let mut left = keymaps_width - keyevent.width() as u16;
32+ for _ in 0..lpad {
33+ text.push(' ');
34+ }
35+ write!(text, "{}", keyevent).ok();
36+ for keyevent in &keyevents[1..] {
37+ write!(text, ", {}", keyevent).ok();
38+ left -= 2 + keyevent.width() as u16;
39+ }
40+ for _ in 0..left + mpad {
41+ text.push(' ');
42+ }
43+ let desc = desc.trim();
44+ let w = lpad + keymaps_width + mpad + (desc.width() as u16) + rpad;
45+ if w > width {
46+ width = w;
47+ }
48+ writeln!(text, "{}", desc).ok();
49+ }
50+ Info {
51+ title,
52+ text,
53+ width,
54+ height,
55+ }
56+ }
57+}
helix-view/src/input.rs+121−44
@@ -1,5 +1,6 @@
1//! Input event handling, currently backed by crossterm.
1//! Input event handling, currently backed by crossterm.
2use anyhow::{anyhow, Error};
2use anyhow::{anyhow, Error};
3+use helix_core::unicode::width::UnicodeWidthStr;
3use serde::de::{self, Deserialize, Deserializer};
4use serde::de::{self, Deserialize, Deserializer};
4use std::fmt;
5use std::fmt;
5
6
@@ -13,6 +14,32 @@ pub struct KeyEvent {
13 pub modifiers: KeyModifiers,
14 pub modifiers: KeyModifiers,
14}
15}
15
16
17+pub(crate) mod keys {
18+ pub(crate) const BACKSPACE: &str = "backspace";
19+ pub(crate) const ENTER: &str = "ret";
20+ pub(crate) const LEFT: &str = "left";
21+ pub(crate) const RIGHT: &str = "right";
22+ pub(crate) const UP: &str = "up";
23+ pub(crate) const DOWN: &str = "down";
24+ pub(crate) const HOME: &str = "home";
25+ pub(crate) const END: &str = "end";
26+ pub(crate) const PAGEUP: &str = "pageup";
27+ pub(crate) const PAGEDOWN: &str = "pagedown";
28+ pub(crate) const TAB: &str = "tab";
29+ pub(crate) const BACKTAB: &str = "backtab";
30+ pub(crate) const DELETE: &str = "del";
31+ pub(crate) const INSERT: &str = "ins";
32+ pub(crate) const NULL: &str = "null";
33+ pub(crate) const ESC: &str = "esc";
34+ pub(crate) const SPACE: &str = "space";
35+ pub(crate) const LESS_THAN: &str = "lt";
36+ pub(crate) const GREATER_THAN: &str = "gt";
37+ pub(crate) const PLUS: &str = "plus";
38+ pub(crate) const MINUS: &str = "minus";
39+ pub(crate) const SEMICOLON: &str = "semicolon";
40+ pub(crate) const PERCENT: &str = "percent";
41+}
42+
16impl fmt::Display for KeyEvent {
43impl fmt::Display for KeyEvent {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
18 f.write_fmt(format_args!(
45 f.write_fmt(format_args!(
@@ -34,28 +61,29 @@ impl fmt::Display for KeyEvent {
34 },
61 },
35 ))?;
62 ))?;
36 match self.code {
63 match self.code {
37− KeyCode::Backspace => f.write_str("backspace")?,
64+ KeyCode::Backspace => f.write_str(keys::BACKSPACE)?,
38− KeyCode::Enter => f.write_str("ret")?,
65+ KeyCode::Enter => f.write_str(keys::ENTER)?,
39− KeyCode::Left => f.write_str("left")?,
66+ KeyCode::Left => f.write_str(keys::LEFT)?,
40− KeyCode::Right => f.write_str("right")?,
67+ KeyCode::Right => f.write_str(keys::RIGHT)?,
41− KeyCode::Up => f.write_str("up")?,
68+ KeyCode::Up => f.write_str(keys::UP)?,
42− KeyCode::Down => f.write_str("down")?,
69+ KeyCode::Down => f.write_str(keys::DOWN)?,
43− KeyCode::Home => f.write_str("home")?,
70+ KeyCode::Home => f.write_str(keys::HOME)?,
44− KeyCode::End => f.write_str("end")?,
71+ KeyCode::End => f.write_str(keys::END)?,
45− KeyCode::PageUp => f.write_str("pageup")?,
72+ KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
46− KeyCode::PageDown => f.write_str("pagedown")?,
73+ KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
47− KeyCode::Tab => f.write_str("tab")?,
74+ KeyCode::Tab => f.write_str(keys::TAB)?,
48− KeyCode::BackTab => f.write_str("backtab")?,
75+ KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
49− KeyCode::Delete => f.write_str("del")?,
76+ KeyCode::Delete => f.write_str(keys::DELETE)?,
50− KeyCode::Insert => f.write_str("ins")?,
77+ KeyCode::Insert => f.write_str(keys::INSERT)?,
51− KeyCode::Null => f.write_str("null")?,
78+ KeyCode::Null => f.write_str(keys::NULL)?,
52− KeyCode::Esc => f.write_str("esc")?,
79+ KeyCode::Esc => f.write_str(keys::ESC)?,
80+ KeyCode::Char(' ') => f.write_str(keys::SPACE)?,
53− KeyCode::Char('<') => f.write_str("lt")?,
81+ KeyCode::Char('<') => f.write_str(keys::LESS_THAN)?,
54− KeyCode::Char('>') => f.write_str("gt")?,
82+ KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?,
55− KeyCode::Char('+') => f.write_str("plus")?,
83+ KeyCode::Char('+') => f.write_str(keys::PLUS)?,
56− KeyCode::Char('-') => f.write_str("minus")?,
84+ KeyCode::Char('-') => f.write_str(keys::MINUS)?,
57− KeyCode::Char(';') => f.write_str("semicolon")?,
85+ KeyCode::Char(';') => f.write_str(keys::SEMICOLON)?,
58− KeyCode::Char('%') => f.write_str("percent")?,
86+ KeyCode::Char('%') => f.write_str(keys::PERCENT)?,
59 KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
87 KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
60 KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
88 KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
61 };
89 };
@@ -63,34 +91,83 @@ impl fmt::Display for KeyEvent {
63 }
91 }
64}
92}
65
93
94+impl UnicodeWidthStr for KeyEvent {
95+ fn width(&self) -> usize {
96+ use helix_core::unicode::width::UnicodeWidthChar;
97+ let mut width = match self.code {
98+ KeyCode::Backspace => keys::BACKSPACE.len(),
99+ KeyCode::Enter => keys::ENTER.len(),
100+ KeyCode::Left => keys::LEFT.len(),
101+ KeyCode::Right => keys::RIGHT.len(),
102+ KeyCode::Up => keys::UP.len(),
103+ KeyCode::Down => keys::DOWN.len(),
104+ KeyCode::Home => keys::HOME.len(),
105+ KeyCode::End => keys::END.len(),
106+ KeyCode::PageUp => keys::PAGEUP.len(),
107+ KeyCode::PageDown => keys::PAGEDOWN.len(),
108+ KeyCode::Tab => keys::TAB.len(),
109+ KeyCode::BackTab => keys::BACKTAB.len(),
110+ KeyCode::Delete => keys::DELETE.len(),
111+ KeyCode::Insert => keys::INSERT.len(),
112+ KeyCode::Null => keys::NULL.len(),
113+ KeyCode::Esc => keys::ESC.len(),
114+ KeyCode::Char(' ') => keys::SPACE.len(),
115+ KeyCode::Char('<') => keys::LESS_THAN.len(),
116+ KeyCode::Char('>') => keys::GREATER_THAN.len(),
117+ KeyCode::Char('+') => keys::PLUS.len(),
118+ KeyCode::Char('-') => keys::MINUS.len(),
119+ KeyCode::Char(';') => keys::SEMICOLON.len(),
120+ KeyCode::Char('%') => keys::PERCENT.len(),
121+ KeyCode::F(1..=9) => 2,
122+ KeyCode::F(_) => 3,
123+ KeyCode::Char(c) => c.width().unwrap_or(0),
124+ };
125+ if self.modifiers.contains(KeyModifiers::SHIFT) {
126+ width += 2;
127+ }
128+ if self.modifiers.contains(KeyModifiers::ALT) {
129+ width += 2;
130+ }
131+ if self.modifiers.contains(KeyModifiers::CONTROL) {
132+ width += 2;
133+ }
134+ width
135+ }
136+
137+ fn width_cjk(&self) -> usize {
138+ self.width()
139+ }
140+}
141+
66impl std::str::FromStr for KeyEvent {
142impl std::str::FromStr for KeyEvent {
67 type Err = Error;
143 type Err = Error;
68
144
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 let mut tokens: Vec<_> = s.split('-').collect();
146 let mut tokens: Vec<_> = s.split('-').collect();
71 let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
147 let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
72− "backspace" => KeyCode::Backspace,
148+ keys::BACKSPACE => KeyCode::Backspace,
73− "space" => KeyCode::Char(' '),
74− "ret" => KeyCode::Enter,
149+ keys::ENTER => KeyCode::Enter,
75− "lt" => KeyCode::Char('<'),
76− "gt" => KeyCode::Char('>'),
77− "plus" => KeyCode::Char('+'),
78− "minus" => KeyCode::Char('-'),
79− "semicolon" => KeyCode::Char(';'),
80− "percent" => KeyCode::Char('%'),
81− "left" => KeyCode::Left,
150+ keys::LEFT => KeyCode::Left,
82− "right" => KeyCode::Right,
151+ keys::RIGHT => KeyCode::Right,
152+ keys::UP => KeyCode::Up,
83− "up" => KeyCode::Down,
153+ keys::DOWN => KeyCode::Down,
84− "home" => KeyCode::Home,
154+ keys::HOME => KeyCode::Home,
85− "end" => KeyCode::End,
155+ keys::END => KeyCode::End,
86− "pageup" => KeyCode::PageUp,
156+ keys::PAGEUP => KeyCode::PageUp,
87− "pagedown" => KeyCode::PageDown,
157+ keys::PAGEDOWN => KeyCode::PageDown,
88− "tab" => KeyCode::Tab,
158+ keys::TAB => KeyCode::Tab,
89− "backtab" => KeyCode::BackTab,
159+ keys::BACKTAB => KeyCode::BackTab,
90− "del" => KeyCode::Delete,
160+ keys::DELETE => KeyCode::Delete,
91− "ins" => KeyCode::Insert,
161+ keys::INSERT => KeyCode::Insert,
92− "null" => KeyCode::Null,
162+ keys::NULL => KeyCode::Null,
93− "esc" => KeyCode::Esc,
163+ keys::ESC => KeyCode::Esc,
164+ keys::SPACE => KeyCode::Char(' '),
165+ keys::LESS_THAN => KeyCode::Char('<'),
166+ keys::GREATER_THAN => KeyCode::Char('>'),
167+ keys::PLUS => KeyCode::Char('+'),
168+ keys::MINUS => KeyCode::Char('-'),
169+ keys::SEMICOLON => KeyCode::Char(';'),
170+ keys::PERCENT => KeyCode::Char('%'),
94 single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
171 single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
95 function if function.len() > 1 && function.starts_with('F') => {
172 function if function.len() > 1 && function.starts_with('F') => {
96 let function: String = function.chars().skip(1).collect();
173 let function: String = function.chars().skip(1).collect();
helix-view/src/lib.rs+1
@@ -5,6 +5,7 @@ pub mod clipboard;
5pub mod document;
5pub mod document;
6pub mod editor;
6pub mod editor;
7pub mod graphics;
7pub mod graphics;
8+pub mod info;
8pub mod input;
9pub mod input;
9pub mod keyboard;
10pub mod keyboard;
10pub mod register_selection;
11pub mod register_selection;
languages.toml+9
@@ -165,6 +165,15 @@ roots = []
165
165
166indent = { tab-width = 4, unit = "\t" }
166indent = { tab-width = 4, unit = "\t" }
167
167
168+[[language]]
169+name = "julia"
170+scope = "source.julia"
171+injection-regex = "julia"
172+file-types = ["jl"]
173+roots = []
174+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);" ] }
175+indent = { tab-width = 2, unit = " " }
176+
168# [[language]]
177# [[language]]
169# name = "haskell"
178# name = "haskell"
170# scope = "source.haskell"
179# scope = "source.haskell"
runtime/queries/julia/folds.scmnew file+11
@@ -0,0 +1,11 @@
1+[
2+ (module_definition)
3+ (struct_definition)
4+ (macro_definition)
5+ (function_definition)
6+ (compound_expression) ; begin blocks
7+ (let_statement)
8+ (if_statement)
9+ (for_statement)
10+ (while_statement)
11+] @fold
runtime/queries/julia/highlights.scmnew file+180
@@ -0,0 +1,180 @@
1+(identifier) @variable
2+;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation)
3+;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables
4+ ;(match? @type "^[A-Z][^_]"))
5+((identifier) @constant
6+ (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$"))
7+
8+[
9+ (triple_string)
10+ (string)
11+] @string
12+
13+(string
14+ prefix: (identifier) @constant.builtin)
15+
16+(macro_identifier) @function.macro
17+(macro_identifier (identifier) @function.macro) ; for any one using the variable highlight
18+(macro_definition
19+ name: (identifier) @function.macro
20+ ["macro" "end" @keyword])
21+
22+(field_expression
23+ (identifier)
24+ (identifier) @field .)
25+
26+(function_definition
27+ name: (identifier) @function)
28+(call_expression
29+ (identifier) @function)
30+(call_expression
31+ (field_expression (identifier) @method .))
32+(broadcast_call_expression
33+ (identifier) @function)
34+(broadcast_call_expression
35+ (field_expression (identifier) @method .))
36+(parameter_list
37+ (identifier) @parameter)
38+(parameter_list
39+ (optional_parameter .
40+ (identifier) @parameter))
41+(typed_parameter
42+ (identifier) @parameter
43+ (identifier) @type)
44+(type_parameter_list
45+ (identifier) @type)
46+(typed_parameter
47+ (identifier) @parameter
48+ (parameterized_identifier) @type)
49+(function_expression
50+ . (identifier) @parameter)
51+(spread_parameter) @parameter
52+(spread_parameter
53+ (identifier) @parameter)
54+(named_argument
55+ . (identifier) @parameter)
56+(argument_list
57+ (typed_expression
58+ (identifier) @parameter
59+ (identifier) @type))
60+(argument_list
61+ (typed_expression
62+ (identifier) @parameter
63+ (parameterized_identifier) @type))
64+
65+;; Symbol expressions (:my-wanna-be-lisp-keyword)
66+(quote_expression
67+ (identifier)) @symbol
68+
69+;; Parsing error! foo (::Type) get's parsed as two quote expressions
70+(argument_list
71+ (quote_expression
72+ (quote_expression
73+ (identifier) @type)))
74+
75+(type_argument_list
76+ (identifier) @type)
77+(parameterized_identifier (_)) @type
78+(argument_list
79+ (typed_expression . (identifier) @parameter))
80+
81+(typed_expression
82+ (identifier) @type .)
83+(typed_expression
84+ (parameterized_identifier) @type .)
85+
86+(struct_definition
87+ name: (identifier) @type)
88+
89+(number) @number
90+(range_expression
91+ (identifier) @number
92+ (eq? @number "end"))
93+(range_expression
94+ (_
95+ (identifier) @number
96+ (eq? @number "end")))
97+(coefficient_expression
98+ (number)
99+ (identifier) @constant.builtin)
100+
101+;; TODO: operators.
102+;; Those are a bit difficult to implement since the respective nodes are hidden right now (_power_operator)
103+;; and heavily use Unicode chars (support for those are bad in vim/lua regexes)
104+;[;
105+ ;(power_operator);
106+ ;(times_operator);
107+ ;(plus_operator);
108+ ;(arrow_operator);
109+ ;(comparison_operator);
110+ ;(assign_operator);
111+;] @operator ;
112+
113+"end" @keyword
114+
115+(if_statement
116+ ["if" "end"] @conditional)
117+(elseif_clause
118+ ["elseif"] @conditional)
119+(else_clause
120+ ["else"] @conditional)
121+(ternary_expression
122+ ["?" ":"] @conditional)
123+
124+(function_definition ["function" "end"] @keyword.function)
125+
126+(comment) @comment
127+
128+[
129+ "const"
130+ "return"
131+ "macro"
132+ "struct"
133+ "primitive"
134+ "type"
135+] @keyword
136+
137+((identifier) @keyword (#any-of? @keyword "global" "local"))
138+
139+(compound_expression
140+ ["begin" "end"] @keyword)
141+(try_statement
142+ ["try" "end" ] @exception)
143+(finally_clause
144+ "finally" @exception)
145+(catch_clause
146+ "catch" @exception)
147+(quote_statement
148+ ["quote" "end"] @keyword)
149+(let_statement
150+ ["let" "end"] @keyword)
151+(for_statement
152+ ["for" "end"] @repeat)
153+(while_statement
154+ ["while" "end"] @repeat)
155+(break_statement) @repeat
156+(continue_statement) @repeat
157+(for_binding
158+ "in" @repeat)
159+(for_clause
160+ "for" @repeat)
161+(do_clause
162+ ["do" "end"] @keyword)
163+
164+(export_statement
165+ ["export"] @include)
166+
167+[
168+ "using"
169+ "module"
170+ "import"
171+] @include
172+
173+((identifier) @include (#eq? @include "baremodule"))
174+
175+(((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$"))
176+(((identifier) @boolean) (eq? @boolean "true"))
177+(((identifier) @boolean) (eq? @boolean "false"))
178+
179+["::" ":" "." "," "..." "!"] @punctuation.delimiter
180+["[" "]" "(" ")" "{" "}"] @punctuation.bracket
runtime/queries/julia/injections.scmnew file+5
@@ -0,0 +1,5 @@
1+; TODO: re-add when markdown is added.
2+; ((triple_string) @markdown
3+; (#offset! @markdown 0 3 0 -3))
4+
5+(comment) @comment
runtime/queries/julia/locals.scmnew file+59
@@ -0,0 +1,59 @@
1+
2+(import_statement
3+ (identifier) @definition.import)
4+(variable_declaration
5+ (identifier) @definition.var)
6+(variable_declaration
7+ (tuple_expression
8+ (identifier) @definition.var))
9+(for_binding
10+ (identifier) @definition.var)
11+(for_binding
12+ (tuple_expression
13+ (identifier) @definition.var))
14+
15+(assignment_expression
16+ (tuple_expression
17+ (identifier) @definition.var))
18+(assignment_expression
19+ (bare_tuple_expression
20+ (identifier) @definition.var))
21+(assignment_expression
22+ (identifier) @definition.var)
23+
24+(type_parameter_list
25+ (identifier) @definition.type)
26+(type_argument_list
27+ (identifier) @definition.type)
28+(struct_definition
29+ name: (identifier) @definition.type)
30+
31+(parameter_list
32+ (identifier) @definition.parameter)
33+(typed_parameter
34+ (identifier) @definition.parameter
35+ (identifier))
36+(function_expression
37+ . (identifier) @definition.parameter)
38+(argument_list
39+ (typed_expression
40+ (identifier) @definition.parameter
41+ (identifier)))
42+(spread_parameter
43+ (identifier) @definition.parameter)
44+
45+(function_definition
46+ name: (identifier) @definition.function) @scope
47+(macro_definition
48+ name: (identifier) @definition.macro) @scope
49+
50+(identifier) @reference
51+
52+[
53+ (try_statement)
54+ (finally_clause)
55+ (quote_statement)
56+ (let_statement)
57+ (compound_expression)
58+ (for_statement)
59+] @scope
runtime/themes/onedark.toml+16−6
@@ -29,16 +29,26 @@
29"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
29"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
30"error" = { fg = "#e06c75", modifiers = ['bold'] }
30"error" = { fg = "#e06c75", modifiers = ['bold'] }
31
31
32−"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
33"ui.background" = { fg = "#ABB2BF", bg = "#282C34" }
32"ui.background" = { fg = "#ABB2BF", bg = "#282C34" }
33+
34+"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] }
35+"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] }
36+"ui.cursor.match" = { fg = "#61AFEF", modifiers = ['underlined']}
37+
38+"ui.selection" = { bg = "#5C6370" }
34−"ui.help" = { bg = "#3E4452" }
39+"ui.selection.primary" = { bg = "#3E4452" }
40+
35"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
41"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
36"ui.linenr.selected" = { fg = "#ABB2BF" }
42"ui.linenr.selected" = { fg = "#ABB2BF" }
37−"ui.popup" = { bg = "#3E4452" }
43+
38"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
44"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
39−"ui.statusline.inactive" = { fg = "#ABB2Bf", bg = "#2C323C" }
45+"ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" }
40−"ui.selection" = { bg = "#3E4452" }
46+
41"ui.text" = { fg = "#ABB2BF", bg = "#282C34" }
47"ui.text" = { fg = "#ABB2BF", bg = "#282C34" }
42"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
48"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
49+
50+"ui.help" = { bg = "#3E4452" }
51+"ui.popup" = { bg = "#3E4452" }
43"ui.window" = { bg = "#3E4452" }
52"ui.window" = { bg = "#3E4452" }
44−# "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported
53+"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
54+