From 07fe4a6a40b4a3e3dd45a8e9f7e7c20a2124bd73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kangwook=20Lee=20=28=EC=9D=B4=EA=B0=95=EC=9A=B1=29?= Date: Sat, 4 Sep 2021 22:30:32 +0900 Subject: [PATCH 01/92] Add commands that extends to long words (#706) --- helix-term/src/commands.rs | 42 ++++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 3 +++ 2 files changed, 45 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 116f39bd..dfbfe1d5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -180,6 +180,9 @@ impl Command { move_next_long_word_end, "Move to end of next long word", extend_next_word_start, "Extend to beginning of next word", extend_prev_word_start, "Extend to beginning of previous word", + extend_next_long_word_start, "Extend to beginning of next long word", + extend_prev_long_word_start, "Extend to beginning of previous long word", + extend_next_long_word_end, "Extend to end of next long word", extend_next_word_end, "Extend to end of next word", find_till_char, "Move till next occurance of char", find_next_char, "Move to next occurance of char", @@ -622,6 +625,45 @@ fn extend_next_word_end(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn extend_next_long_word_start(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let word = movement::move_next_long_word_start(text, range, count); + let pos = word.cursor(text); + range.put_cursor(text, pos, true) + }); + doc.set_selection(view.id, selection); +} + +fn extend_prev_long_word_start(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let word = movement::move_prev_long_word_start(text, range, count); + let pos = word.cursor(text); + range.put_cursor(text, pos, true) + }); + doc.set_selection(view.id, selection); +} + +fn extend_next_long_word_end(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let word = movement::move_next_long_word_end(text, range, count); + let pos = word.cursor(text); + range.put_cursor(text, pos, true) + }); + doc.set_selection(view.id, selection); +} + #[inline] fn find_char_impl(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 71ac01a9..a936dccc 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -527,6 +527,9 @@ impl Default for Keymaps { "w" => extend_next_word_start, "b" => extend_prev_word_start, "e" => extend_next_word_end, + "W" => extend_next_long_word_start, + "B" => extend_prev_long_word_start, + "E" => extend_next_long_word_end, "t" => extend_till_char, "f" => extend_next_char, From ea2b4c687d4ee360cc8cbdbd6cf1c1cb2728a23d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 4 Sep 2021 20:16:43 +0530 Subject: [PATCH 02/92] Refactor {move,extend}_char_* commands --- helix-term/src/commands.rs | 103 ++++++++++++------------------------- 1 file changed, 32 insertions(+), 71 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index dfbfe1d5..c248fe18 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -358,48 +358,53 @@ impl PartialEq for Command { } } -fn move_char_left(cx: &mut Context) { +fn move_impl(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) +where + F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move) - }); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| move_fn(text, range, dir, count, behaviour)); doc.set_selection(view.id, selection); } -fn move_char_right(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +use helix_core::movement::{move_horizontally, move_vertically}; - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn move_char_left(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Backward, Movement::Move) } -fn move_line_up(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn move_char_right(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Forward, Movement::Move) +} - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn move_line_up(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Backward, Movement::Move) } fn move_line_down(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + move_impl(cx, move_vertically, Direction::Forward, Movement::Move) +} - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn extend_char_left(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend) +} + +fn extend_char_right(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend) +} + +fn extend_line_up(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Backward, Movement::Extend) +} + +fn extend_line_down(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) } fn goto_line_end(cx: &mut Context) { @@ -1001,28 +1006,6 @@ fn half_page_down(cx: &mut Context) { scroll(cx, offset, Direction::Forward); } -fn extend_char_left(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - -fn extend_char_right(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - fn copy_selection_on_line(cx: &mut Context, direction: Direction) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -1093,28 +1076,6 @@ fn copy_selection_on_next_line(cx: &mut Context) { copy_selection_on_line(cx, Direction::Forward) } -fn extend_line_up(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - -fn extend_line_down(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - fn select_all(cx: &mut Context) { let (view, doc) = current!(cx.editor); From 33ce8779fd43ae330fb21c698e0ce117ae40abc4 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 4 Sep 2021 20:29:11 +0530 Subject: [PATCH 03/92] Refactor {move,extend}_word_* commands --- helix-term/src/commands.rs | 127 ++++++++----------------------------- 1 file changed, 28 insertions(+), 99 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c248fe18..cad8cf60 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -503,7 +503,10 @@ fn goto_window_bottom(cx: &mut Context) { goto_window(cx, Align::Bottom) } -fn move_next_word_start(cx: &mut Context) { +fn move_word_impl(cx: &mut Context, move_fn: F) +where + F: Fn(RopeSlice, Range, usize) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -511,68 +514,32 @@ fn move_next_word_start(cx: &mut Context) { let selection = doc .selection(view.id) .clone() - .transform(|range| movement::move_next_word_start(text, range, count)); + .transform(|range| move_fn(text, range, count)); doc.set_selection(view.id, selection); } -fn move_prev_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn move_next_word_start(cx: &mut Context) { + move_word_impl(cx, movement::move_next_word_start) +} - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_prev_word_start(text, range, count)); - doc.set_selection(view.id, selection); +fn move_prev_word_start(cx: &mut Context) { + move_word_impl(cx, movement::move_prev_word_start) } fn move_next_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_word_end(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_word_end) } fn move_next_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_long_word_start(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_long_word_start) } fn move_prev_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_prev_long_word_start(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_prev_long_word_start) } fn move_next_long_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_long_word_end(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_long_word_end) } fn goto_file_start(cx: &mut Context) { @@ -591,82 +558,44 @@ fn goto_file_end(cx: &mut Context) { doc.set_selection(view.id, Selection::point(doc.text().len_chars())); } -fn extend_next_word_start(cx: &mut Context) { +fn extend_word_impl(cx: &mut Context, extend_fn: F) +where + F: Fn(RopeSlice, Range, usize) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_word_start(text, range, count); + let word = extend_fn(text, range, count); let pos = word.cursor(text); range.put_cursor(text, pos, true) }); doc.set_selection(view.id, selection); } -fn extend_prev_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn extend_next_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_next_word_start) +} - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_prev_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); +fn extend_prev_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_prev_word_start) } fn extend_next_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_word_end(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_next_word_end) } fn extend_next_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_long_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_next_long_word_start) } fn extend_prev_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_prev_long_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_prev_long_word_start) } fn extend_next_long_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_long_word_end(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_next_long_word_end) } #[inline] From 95cd2c645b853b8b31792a5f97a144676198927f Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 4 Sep 2021 20:59:08 +0530 Subject: [PATCH 04/92] Refactor switch_case commands --- helix-term/src/commands.rs | 48 +++++++++++++++----------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cad8cf60..2fbed6b3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -811,12 +811,25 @@ fn replace(cx: &mut Context) { }) } -fn switch_case(cx: &mut Context) { +fn switch_case_impl(cx: &mut Context, change_fn: F) +where + F: Fn(Cow) -> Tendril, +{ let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range - .fragment(doc.text().slice(..)) + let text: Tendril = change_fn(range.fragment(doc.text().slice(..))); + + (range.from(), range.to(), Some(text)) + }); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); +} + +fn switch_case(cx: &mut Context) { + switch_case_impl(cx, |string| { + string .chars() .flat_map(|ch| { if ch.is_lowercase() { @@ -827,39 +840,16 @@ fn switch_case(cx: &mut Context) { vec![ch] } }) - .collect(); - - (range.from(), range.to(), Some(text)) + .collect() }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn switch_to_uppercase(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into(); - - (range.from(), range.to(), Some(text)) - }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + switch_case_impl(cx, |string| string.to_uppercase().into()); } fn switch_to_lowercase(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into(); - - (range.from(), range.to(), Some(text)) - }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + switch_case_impl(cx, |string| string.to_lowercase().into()); } pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { From e40e6db2271b9568352b7477ed8b9f9895881cf9 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Thu, 2 Sep 2021 22:05:33 +0530 Subject: [PATCH 05/92] feat: Default theme palette using 16 terminal colors --- book/src/themes.md | 21 +++++++++++++++++++++ helix-view/src/graphics.rs | 6 +++--- helix-view/src/theme.rs | 20 +++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 0a4d58ad..8642e659 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -123,3 +123,24 @@ black = "#000000" Remember that the `[palette]` table includes all keys after its header, so you should define the palette after normal theme options. + +If there is no `[palette]` section, a default palette which uses the terminal's default 16 colors are used: + +| Color Name | +| --- | +| `black` | +| `red` | +| `green` | +| `yellow` | +| `blue` | +| `magenta` | +| `cyan` | +| `gray` | +| `light-red` | +| `light-green` | +| `light-yellow` | +| `light-blue` | +| `light-magenta` | +| `light-cyan` | +| `light-gray` | +| `white` | diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 66013ee5..0bfca04a 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -224,13 +224,13 @@ pub enum Color { Magenta, Cyan, Gray, - DarkGray, LightRed, LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, + LightGray, White, Rgb(u8, u8, u8), Indexed(u8), @@ -250,14 +250,14 @@ impl From for crossterm::style::Color { Color::Blue => CColor::DarkBlue, Color::Magenta => CColor::DarkMagenta, Color::Cyan => CColor::DarkCyan, - Color::Gray => CColor::Grey, - Color::DarkGray => CColor::DarkGrey, + Color::Gray => CColor::DarkGrey, Color::LightRed => CColor::Red, Color::LightGreen => CColor::Green, Color::LightBlue => CColor::Blue, Color::LightYellow => CColor::Yellow, Color::LightMagenta => CColor::Magenta, Color::LightCyan => CColor::Cyan, + Color::LightGray => CColor::Grey, Color::White => CColor::White, Color::Indexed(i) => CColor::AnsiValue(i), Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 74b817d0..9f768505 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::Context; +use helix_core::hashmap; use log::warn; use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; @@ -142,7 +143,24 @@ struct ThemePalette { impl Default for ThemePalette { fn default() -> Self { - Self::new(HashMap::new()) + Self::new(hashmap! { + "black".to_string() => Color::Black, + "red".to_string() => Color::Red, + "green".to_string() => Color::Green, + "yellow".to_string() => Color::Yellow, + "blue".to_string() => Color::Blue, + "magenta".to_string() => Color::Magenta, + "cyan".to_string() => Color::Cyan, + "gray".to_string() => Color::Gray, + "light-red".to_string() => Color::LightRed, + "light-green".to_string() => Color::LightGreen, + "light-yellow".to_string() => Color::LightYellow, + "light-blue".to_string() => Color::LightBlue, + "light-magenta".to_string() => Color::LightMagenta, + "light-cyan".to_string() => Color::LightCyan, + "light-gray".to_string() => Color::LightGray, + "white".to_string() => Color::White, + }) } } From e4e93e176ceca39a834abf7e67bf9bfaa34d886d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 3 Sep 2021 16:42:29 +0530 Subject: [PATCH 06/92] fix: Merge default palette with user palette --- book/src/themes.md | 4 +++- helix-view/src/theme.rs | 45 ++++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 8642e659..804baa1c 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -124,7 +124,9 @@ black = "#000000" Remember that the `[palette]` table includes all keys after its header, so you should define the palette after normal theme options. -If there is no `[palette]` section, a default palette which uses the terminal's default 16 colors are used: +The default palette uses the terminal's default 16 colors, and the colors names +are listed below. The `[palette]` section in the config file takes precedence +over it and is merged into the default palette. | Color Name | | --- | diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 9f768505..9c33685b 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -143,30 +143,37 @@ struct ThemePalette { impl Default for ThemePalette { fn default() -> Self { - Self::new(hashmap! { - "black".to_string() => Color::Black, - "red".to_string() => Color::Red, - "green".to_string() => Color::Green, - "yellow".to_string() => Color::Yellow, - "blue".to_string() => Color::Blue, - "magenta".to_string() => Color::Magenta, - "cyan".to_string() => Color::Cyan, - "gray".to_string() => Color::Gray, - "light-red".to_string() => Color::LightRed, - "light-green".to_string() => Color::LightGreen, - "light-yellow".to_string() => Color::LightYellow, - "light-blue".to_string() => Color::LightBlue, - "light-magenta".to_string() => Color::LightMagenta, - "light-cyan".to_string() => Color::LightCyan, - "light-gray".to_string() => Color::LightGray, - "white".to_string() => Color::White, - }) + Self { + palette: hashmap! { + "black".to_string() => Color::Black, + "red".to_string() => Color::Red, + "green".to_string() => Color::Green, + "yellow".to_string() => Color::Yellow, + "blue".to_string() => Color::Blue, + "magenta".to_string() => Color::Magenta, + "cyan".to_string() => Color::Cyan, + "gray".to_string() => Color::Gray, + "light-red".to_string() => Color::LightRed, + "light-green".to_string() => Color::LightGreen, + "light-yellow".to_string() => Color::LightYellow, + "light-blue".to_string() => Color::LightBlue, + "light-magenta".to_string() => Color::LightMagenta, + "light-cyan".to_string() => Color::LightCyan, + "light-gray".to_string() => Color::LightGray, + "white".to_string() => Color::White, + }, + } } } impl ThemePalette { pub fn new(palette: HashMap) -> Self { - Self { palette } + let ThemePalette { + palette: mut default, + } = ThemePalette::default(); + + default.extend(palette); + Self { palette: default } } pub fn hex_string_to_rgb(s: &str) -> Result { From 99a753a5797d38885560e9026b86952615032556 Mon Sep 17 00:00:00 2001 From: oberblastmeister <61095988+oberblastmeister@users.noreply.github.com> Date: Sat, 4 Sep 2021 23:42:33 -0400 Subject: [PATCH 07/92] Document macros (#693) * add docs * clean up * remove * more * Update helix-view/src/macros.rs Co-authored-by: Ivan Tham Co-authored-by: Ivan Tham --- helix-view/src/editor.rs | 5 ----- helix-view/src/macros.rs | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e5ff93ad..562c3c60 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -345,11 +345,6 @@ impl Editor { .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) } - // pub fn current_document(&self) -> Document { - // let id = self.view().doc; - // let doc = &mut editor.documents[id]; - // } - pub fn cursor(&self) -> (Option, CursorKind) { let view = view!(self); let doc = &self.documents[view.doc]; diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index a06d37e7..c9a04270 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -1,3 +1,14 @@ +//! These are macros to make getting very nested fields in the `Editor` struct easier +//! These are macros instead of functions because functions will have to take `&mut self` +//! However, rust doesn't know that you only want a partial borrow instead of borrowing the +//! entire struct which `&mut self` says. This makes it impossible to do other mutable +//! stuff to the struct because it is already borrowed. Because macros are expanded, +//! this circumvents the problem because it is just like indexing fields by hand and then +//! putting a `&mut` in front of it. This way rust can see that we are only borrowing a +//! part of the struct and not the entire thing. + +/// Get the current view and document mutably as a tuple. +/// Returns `(&mut View, &mut Document)` #[macro_export] macro_rules! current { ( $( $editor:ident ).+ ) => {{ @@ -7,6 +18,8 @@ macro_rules! current { }}; } +/// Get the current document mutably. +/// Returns `&mut Document` #[macro_export] macro_rules! doc_mut { ( $( $editor:ident ).+ ) => {{ @@ -14,6 +27,8 @@ macro_rules! doc_mut { }}; } +/// Get the current view mutably. +/// Returns `&mut View` #[macro_export] macro_rules! view_mut { ( $( $editor:ident ).+ ) => {{ @@ -21,6 +36,8 @@ macro_rules! view_mut { }}; } +/// Get the current view immutably +/// Returns `&View` #[macro_export] macro_rules! view { ( $( $editor:ident ).+ ) => {{ From 183dcce992d7c5b2065a93c5835d61e8ee4e9f05 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 5 Sep 2021 09:25:13 +0530 Subject: [PATCH 08/92] Add a sticky mode for keymaps (#635) --- helix-term/src/keymap.rs | 113 +++++++++++++++++++++++++++--------- helix-term/src/ui/editor.rs | 36 +++++++----- 2 files changed, 104 insertions(+), 45 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a936dccc..f0f980bd 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -4,6 +4,7 @@ use helix_core::hashmap; use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ + borrow::Cow, collections::HashMap, ops::{Deref, DerefMut}, }; @@ -47,13 +48,13 @@ macro_rules! keymap { }; (@trie - { $label:literal $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { - keymap!({ $label $($($key)|+ => $value,)+ }) + keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) }; ( - { $label:literal $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { // modified from the hashmap! macro { @@ -70,7 +71,9 @@ macro_rules! keymap { _order.push(_key); )+ )* - $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order)) + let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); + $( _node.is_sticky = $sticky; )? + $crate::keymap::KeyTrie::Node(_node) } }; } @@ -84,6 +87,8 @@ pub struct KeyTrieNode { map: HashMap, #[serde(skip)] order: Vec, + #[serde(skip)] + pub is_sticky: bool, } impl KeyTrieNode { @@ -92,6 +97,7 @@ impl KeyTrieNode { name: name.to_string(), map, order, + is_sticky: false, } } @@ -119,12 +125,10 @@ impl KeyTrieNode { } } } -} -impl From for Info { - fn from(node: KeyTrieNode) -> Self { - let mut body: Vec<(&str, Vec)> = Vec::with_capacity(node.len()); - for (&key, trie) in node.iter() { + pub fn infobox(&self) -> Info { + let mut body: Vec<(&str, Vec)> = Vec::with_capacity(self.len()); + for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => cmd.doc(), KeyTrie::Node(n) => n.name(), @@ -136,16 +140,16 @@ impl From for Info { } } body.sort_unstable_by_key(|(_, keys)| { - node.order.iter().position(|&k| k == keys[0]).unwrap() + self.order.iter().position(|&k| k == keys[0]).unwrap() }); - let prefix = format!("{} ", node.name()); + let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { body = body .into_iter() .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .collect(); } - Info::new(node.name(), body) + Info::new(self.name(), body) } } @@ -218,7 +222,7 @@ impl KeyTrie { } #[derive(Debug, Clone, PartialEq)] -pub enum KeymapResult { +pub enum KeymapResultKind { /// Needs more keys to execute a command. Contains valid keys for next keystroke. Pending(KeyTrieNode), Matched(Command), @@ -229,14 +233,31 @@ pub enum KeymapResult { Cancelled(Vec), } +/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a +/// reference to the sticky node if one is currently active. +pub struct KeymapResult<'a> { + pub kind: KeymapResultKind, + pub sticky: Option<&'a KeyTrieNode>, +} + +impl<'a> KeymapResult<'a> { + pub fn new(kind: KeymapResultKind, sticky: Option<&'a KeyTrieNode>) -> Self { + Self { kind, sticky } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Keymap { /// Always a Node #[serde(flatten)] root: KeyTrie, - /// Stores pending keys waiting for the next key + /// Stores pending keys waiting for the next key. This is relative to a + /// sticky node if one is in use. #[serde(skip)] state: Vec, + /// Stores the sticky node if one is activated. + #[serde(skip)] + sticky: Option, } impl Keymap { @@ -244,6 +265,7 @@ impl Keymap { Keymap { root, state: Vec::new(), + sticky: None, } } @@ -251,27 +273,60 @@ impl Keymap { &self.root } + pub fn sticky(&self) -> Option<&KeyTrieNode> { + self.sticky.as_ref() + } + /// Returns list of keys waiting to be disambiguated. pub fn pending(&self) -> &[KeyEvent] { &self.state } - /// Lookup `key` in the keymap to try and find a command to execute + /// Lookup `key` in the keymap to try and find a command to execute. Escape + /// key cancels pending keystrokes. If there are no pending keystrokes but a + /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { - let &first = self.state.get(0).unwrap_or(&key); - let trie = match self.root.search(&[first]) { - Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd), - None => return KeymapResult::NotFound, + if let key!(Esc) = key { + if self.state.is_empty() { + self.sticky = None; + } + return KeymapResult::new( + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ); + } + + let first = self.state.get(0).unwrap_or(&key); + let trie_node = match self.sticky { + Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), + None => Cow::Borrowed(&self.root), + }; + + let trie = match trie_node.search(&[*first]) { + Some(&KeyTrie::Leaf(cmd)) => { + return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) + } + None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()), Some(t) => t, }; + self.state.push(key); match trie.search(&self.state[1..]) { - Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()), - Some(&KeyTrie::Leaf(command)) => { + Some(&KeyTrie::Node(ref map)) => { + if map.is_sticky { + self.state.clear(); + self.sticky = Some(map.clone()); + } + KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) + } + Some(&KeyTrie::Leaf(cmd)) => { self.state.clear(); - KeymapResult::Matched(command) + return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), + None => KeymapResult::new( + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ), } } @@ -602,19 +657,19 @@ fn merge_partial_keys() { let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); assert_eq!( - keymap.get(key!('i')), - KeymapResult::Matched(Command::normal_mode), + keymap.get(key!('i')).kind, + KeymapResultKind::Matched(Command::normal_mode), "Leaf should replace leaf" ); assert_eq!( - keymap.get(key!('无')), - KeymapResult::Matched(Command::insert_mode), + keymap.get(key!('无')).kind, + KeymapResultKind::Matched(Command::insert_mode), "New leaf should be present in merged keymap" ); // Assumes that z is a node in the default keymap assert_eq!( - keymap.get(key!('z')), - KeymapResult::Matched(Command::jump_backward), + keymap.get(key!('z')).kind, + KeymapResultKind::Matched(Command::jump_backward), "Leaf should replace node" ); // Assumes that `g` is a node in default keymap diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4b9c56e7..e8cd40cf 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -2,7 +2,7 @@ use crate::{ commands, compositor::{Component, Context, EventResult}, key, - keymap::{KeymapResult, Keymaps}, + keymap::{KeymapResult, KeymapResultKind, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -638,7 +638,7 @@ impl EditorView { /// Handle events by looking them up in `self.keymaps`. Returns None /// if event was handled (a command was executed or a subkeymap was - /// activated). Only KeymapResult::{NotFound, Cancelled} is returned + /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned /// otherwise. fn handle_keymap_event( &mut self, @@ -647,29 +647,32 @@ impl EditorView { event: KeyEvent, ) -> Option { self.autoinfo = None; - match self.keymaps.get_mut(&mode).unwrap().get(event) { - KeymapResult::Matched(command) => command.execute(cxt), - KeymapResult::Pending(node) => self.autoinfo = Some(node.into()), - k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k), + let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); + self.autoinfo = key_result.sticky.map(|node| node.infobox()); + + match &key_result.kind { + KeymapResultKind::Matched(command) => command.execute(cxt), + KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), + KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result), } None } fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { - match keyresult { - KeymapResult::NotFound => { + match keyresult.kind { + KeymapResultKind::NotFound => { if let Some(ch) = event.char() { commands::insert::insert_char(cx, ch) } } - KeymapResult::Cancelled(pending) => { + KeymapResultKind::Cancelled(pending) => { for ev in pending { match ev.char() { Some(ch) => commands::insert::insert_char(cx, ch), None => { - if let KeymapResult::Matched(command) = - self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev) + if let KeymapResultKind::Matched(command) = + self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind { command.execute(cx); } @@ -976,11 +979,12 @@ impl Component for EditorView { // how we entered insert mode is important, and we should track that so // we can repeat the side effect. - self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) { - KeymapResult::Matched(command) => command, - // FIXME: insert mode can only be entered through single KeyCodes - _ => unimplemented!(), - }; + self.last_insert.0 = + match self.keymaps.get_mut(&mode).unwrap().get(key).kind { + KeymapResultKind::Matched(command) => command, + // FIXME: insert mode can only be entered through single KeyCodes + _ => unimplemented!(), + }; self.last_insert.1.clear(); } (Mode::Insert, Mode::Normal) => { From 6e21a748b87a4eb9381ea0d24117711c5b547ab1 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 5 Sep 2021 17:50:11 +0530 Subject: [PATCH 09/92] Fix escape not exiting insert mode (#712) Regression due to #635 where escape key in insert mode would not exit normal mode. This happened due to hard coding the escape key to cancel a sticky keymap node. --- helix-term/src/keymap.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f0f980bd..aa60482d 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -287,13 +287,14 @@ impl Keymap { /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { if let key!(Esc) = key { - if self.state.is_empty() { - self.sticky = None; + if !self.state.is_empty() { + return KeymapResult::new( + // Note that Esc is not included here + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ); } - return KeymapResult::new( - KeymapResultKind::Cancelled(self.state.drain(..).collect()), - self.sticky(), - ); + self.sticky = None; } let first = self.state.get(0).unwrap_or(&key); From 08967baef62772177bba95f99990e69094735a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 10:59:29 +0900 Subject: [PATCH 10/92] flake: Update dependencies --- flake.lock | 24 ++++++++++++------------ rust-toolchain.toml | 3 +++ 2 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/flake.lock b/flake.lock index 054a5154..d5271a92 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1625086391, - "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=", + "lastModified": 1630239564, + "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=", "owner": "numtide", "repo": "devshell", - "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7", + "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73", "type": "github" }, "original": { @@ -40,11 +40,11 @@ "rustOverlay": "rustOverlay" }, "locked": { - "lastModified": 1628489367, - "narHash": "sha256-ADYKHf8aPo1qTw1J+eqVprnEbH8lES0yZamD/yM7RAM=", + "lastModified": 1630822133, + "narHash": "sha256-CoPUuZvXx690gj5O1EljXNyGInBDfqWC4MOXARMrHog=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "0dc8383aae5f791a48e34120edb04670b947dc0b", + "rev": "e9e445cd6694171a3393df8fd016356baec5742f", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1628465643, - "narHash": "sha256-QSNw9bDq9uGUniQQtakRuw4m21Jxugm23SXLVgEV4DM=", + "lastModified": 1630850248, + "narHash": "sha256-OzJi6Olf7mSVhGt3W7qOMVP5Qk1lH60zlHeCcITzfv0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6ef4f522d63f22b40004319778761040d3197390", + "rev": "23d5823337f4502dfa17e192d8c53a47cabcb6b4", "type": "github" }, "original": { @@ -79,11 +79,11 @@ "rustOverlay": { "flake": false, "locked": { - "lastModified": 1628475192, - "narHash": "sha256-A32shcfPMCll7psCS0OBxVCkA+PKfeWvmU4y9lgNZzU=", + "lastModified": 1630808055, + "narHash": "sha256-axfnFXuakMyLMlNIr1yVzvC8ipj6nVJnia2YtkWdzYg=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "56a8ddb827cbe7a914be88f4a52998a5f93ff468", + "rev": "b4c01ab310031a557b6a00062378535be33c36bb", "type": "github" }, "original": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..a63c14b9 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +# components = ["rustfmt", "rust-src"] From 57ed5180e0e1af3b3ddcb478c4a6d6ecd969cd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 11:00:33 +0900 Subject: [PATCH 11/92] lsp: Improve line ending handling when generating TextEdit --- helix-lsp/src/client.rs | 10 +++++++--- helix-view/src/document.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index fd34f45d..52b2c1c9 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -3,7 +3,7 @@ use crate::{ Call, Error, OffsetEncoding, Result, }; -use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope}; +use helix_core::{find_root, ChangeSet, Rope}; use jsonrpc_core as jsonrpc; use lsp_types as lsp; use serde_json::Value; @@ -356,7 +356,6 @@ impl Client { // // Calculation is therefore a bunch trickier. - // TODO: stolen from syntax.rs, share use helix_core::RopeSlice; fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position { let lsp::Position { @@ -366,7 +365,12 @@ impl Client { let mut chars = text.chars().peekable(); while let Some(ch) = chars.next() { - if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) { + // LSP only considers \n, \r or \r\n as line endings + if ch == '\n' || ch == '\r' { + // consume a \r\n + if chars.peek() == Some(&'\n') { + chars.next(); + } line += 1; character = 0; } else { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e890a336..b2c02927 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -891,6 +891,40 @@ impl Default for Document { mod test { use super::*; + #[test] + fn changeset_to_changes_ignore_line_endings() { + use helix_lsp::{lsp, Client, OffsetEncoding}; + let text = Rope::from("hello\r\nworld"); + let mut doc = Document::from(text, None); + let view = ViewId::default(); + doc.set_selection(view, Selection::single(0, 0)); + + let transaction = + Transaction::change(doc.text(), vec![(5, 7, Some("\n".into()))].into_iter()); + let old_doc = doc.text().clone(); + doc.apply(&transaction, view); + let changes = Client::changeset_to_changes( + &old_doc, + doc.text(), + transaction.changes(), + OffsetEncoding::Utf8, + ); + + assert_eq!(doc.text(), "hello\nworld"); + + assert_eq!( + changes, + &[lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + lsp::Position::new(0, 5), + lsp::Position::new(1, 0) + )), + text: "\n".into(), + range_length: None, + }] + ); + } + #[test] fn changeset_to_changes() { use helix_lsp::{lsp, Client, OffsetEncoding}; From 3cb95be452491a72d18c98ebc619b0d2abb1b746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 13:18:16 +0900 Subject: [PATCH 12/92] Update tree-sitter to 0.20 0.20 includes querying improvements, we no longer have to convert fragments to strings but can return an iterator of chunks instead. --- Cargo.lock | 4 +- helix-core/Cargo.toml | 2 +- helix-core/src/syntax.rs | 79 +++++++++++++++++++++------------------- helix-syntax/Cargo.toml | 2 +- 4 files changed, 45 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 298ad15b..1b2492b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1034,9 +1034,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.19.5" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0" +checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9" dependencies = [ "cc", "regex", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 8c83816c..5d2db642 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -22,7 +22,7 @@ unicode-segmentation = "1.8" unicode-width = "0.1" unicode-general-category = "0.4" # slab = "0.4.2" -tree-sitter = "0.19" +tree-sitter = "0.20" once_cell = "1.8" arc-swap = "1" regex = "1" diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 64b921e6..a7a5d022 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -276,16 +276,6 @@ fn byte_range_to_str(range: std::ops::Range, source: RopeSlice) -> Cow(node: Node, source: RopeSlice<'a>) -> Cow<'a, [u8]> { - let start_char = source.byte_to_char(node.start_byte()); - let end_char = source.byte_to_char(node.end_byte()); - let fragment = source.slice(start_char..end_char); - match fragment.as_str() { - Some(fragment) => Cow::Borrowed(fragment.as_bytes()), - None => Cow::Owned(String::from(fragment).into_bytes()), - } -} - impl Syntax { // buffer, grammar, config, grammars, sync_timeout? pub fn new( @@ -380,14 +370,11 @@ impl Syntax { // TODO: if reusing cursors this might need resetting if let Some(range) = &range { - cursor_ref.set_byte_range(range.start, range.end); + cursor_ref.set_byte_range(range.clone()); } let captures = cursor_ref - .captures(query_ref, tree_ref.root_node(), move |n: Node| { - // &source[n.byte_range()] - node_to_bytes(n, source) - }) + .captures(query_ref, tree_ref.root_node(), RopeProvider(source)) .peekable(); // manually craft the root layer based on the existing tree @@ -501,10 +488,7 @@ impl LanguageLayer { // let mut injections_by_pattern_index = // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()]; // let matches = - // cursor.matches(combined_injections_query, tree.root_node(), |n: Node| { - // // &source[n.byte_range()] - // node_to_bytes(n, source) - // }); + // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source)); // for mat in matches { // let entry = &mut injections_by_pattern_index[mat.pattern_index]; // let (language_name, content_node, include_children) = @@ -716,7 +700,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::{iter, mem, ops, str, usize}; use tree_sitter::{ Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, - QueryMatch, Range, Tree, + QueryMatch, Range, TextProvider, Tree, }; const CANCELLATION_CHECK_INTERVAL: usize = 100; @@ -776,7 +760,7 @@ struct LocalScope<'a> { } #[derive(Debug)] -struct HighlightIter<'a, 'tree: 'a, F> +struct HighlightIter<'a, F> where F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, { @@ -784,16 +768,41 @@ where byte_offset: usize, injection_callback: F, cancellation_flag: Option<&'a AtomicUsize>, - layers: Vec>, + layers: Vec>, iter_count: usize, next_event: Option, last_highlight_range: Option<(usize, usize, usize)>, } -struct HighlightIterLayer<'a, 'tree: 'a> { +// Adapter to convert rope chunks to bytes +struct ChunksBytes<'a> { + chunks: ropey::iter::Chunks<'a>, +} +impl<'a> Iterator for ChunksBytes<'a> { + type Item = &'a [u8]; + fn next(&mut self) -> Option { + self.chunks.next().map(str::as_bytes) + } +} + +struct RopeProvider<'a>(RopeSlice<'a>); +impl<'a> TextProvider<'a> for RopeProvider<'a> { + type I = ChunksBytes<'a>; + + fn text(&mut self, node: Node) -> Self::I { + let start_char = self.0.byte_to_char(node.start_byte()); + let end_char = self.0.byte_to_char(node.end_byte()); + let fragment = self.0.slice(start_char..end_char); + ChunksBytes { + chunks: fragment.chunks(), + } + } +} + +struct HighlightIterLayer<'a> { _tree: Option, cursor: QueryCursor, - captures: iter::Peekable>>, + captures: iter::Peekable>>, config: &'a HighlightConfiguration, highlight_end_stack: Vec, scope_stack: Vec>, @@ -801,7 +810,7 @@ struct HighlightIterLayer<'a, 'tree: 'a> { depth: usize, } -impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> { +impl<'a> fmt::Debug for HighlightIterLayer<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HighlightIterLayer").finish() } @@ -972,7 +981,7 @@ impl HighlightConfiguration { } } -impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { +impl<'a> HighlightIterLayer<'a> { /// Create a new 'layer' of highlighting for this document. /// /// In the even that the new layer contains "combined injections" (injections where multiple @@ -1029,10 +1038,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { let matches = cursor.matches( combined_injections_query, tree.root_node(), - |n: Node| { - // &source[n.byte_range()] - node_to_bytes(n, source) - }, + RopeProvider(source), ); for mat in matches { let entry = &mut injections_by_pattern_index[mat.pattern_index]; @@ -1079,10 +1085,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; let captures = cursor_ref - .captures(&config.query, tree_ref.root_node(), move |n: Node| { - // &source[n.byte_range()] - node_to_bytes(n, source) - }) + .captures(&config.query, tree_ref.root_node(), RopeProvider(source)) .peekable(); result.push(HighlightIterLayer { @@ -1236,7 +1239,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { } } -impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F> +impl<'a, F> HighlightIter<'a, F> where F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, { @@ -1287,7 +1290,7 @@ where } } - fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) { + fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) { if let Some(sort_key) = layer.sort_key() { let mut i = 1; while i < self.layers.len() { @@ -1306,7 +1309,7 @@ where } } -impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F> +impl<'a, F> Iterator for HighlightIter<'a, F> where F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, { @@ -1570,7 +1573,7 @@ where fn injection_for_match<'a>( config: &HighlightConfiguration, query: &'a Query, - query_match: &QueryMatch<'a>, + query_match: &QueryMatch<'a, 'a>, source: RopeSlice<'a>, ) -> (Option>, Option>, bool) { let content_capture_index = config.injection_content_capture_index; diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml index 73eda472..9c2b8275 100644 --- a/helix-syntax/Cargo.toml +++ b/helix-syntax/Cargo.toml @@ -11,7 +11,7 @@ homepage = "https://helix-editor.com" include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"] [dependencies] -tree-sitter = "0.19" +tree-sitter = "0.20" libloading = "0.7" anyhow = "1" From fe17b99ab399d1ee5483ad54c6b124af869afdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 15:25:08 +0900 Subject: [PATCH 13/92] fix: lsp: Don't consume \n\n as a single newline --- helix-lsp/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 52b2c1c9..d0a8183f 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -368,7 +368,7 @@ impl Client { // LSP only considers \n, \r or \r\n as line endings if ch == '\n' || ch == '\r' { // consume a \r\n - if chars.peek() == Some(&'\n') { + if ch == '\r' && chars.peek() == Some(&'\n') { chars.next(); } line += 1; From 41f1e8e4fb4b3b387f34ef5d2e913e7ebc7fd888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 14:27:55 +0900 Subject: [PATCH 14/92] fix: lsp: Terminate transport on EOF If stdout/stderr is closed, read_line will return 0 indicating EOF. --- helix-lsp/src/transport.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 67b7b48f..9353de20 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::{Error, Result}; use anyhow::Context; use jsonrpc_core as jsonrpc; use log::{debug, error, info, warn}; @@ -76,14 +76,17 @@ impl Transport { let mut content_length = None; loop { buffer.truncate(0); - reader.read_line(buffer).await?; - let header = buffer.trim(); + if reader.read_line(buffer).await? == 0 { + return Err(Error::StreamClosed); + }; + + // debug!("<- header {:?}", buffer); if header.is_empty() { break; } - debug!("<- header {}", header); + let header = buffer.trim(); let parts = header.split_once(": "); @@ -121,8 +124,10 @@ impl Transport { buffer: &mut String, ) -> Result<()> { buffer.truncate(0); - err.read_line(buffer).await?; - error!("err <- {}", buffer); + if err.read_line(buffer).await? == 0 { + return Err(Error::StreamClosed); + }; + error!("err <- {:?}", buffer); Ok(()) } From c3a58cdadd8be85b79d773122e807862a3da3a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 16:03:06 +0900 Subject: [PATCH 15/92] lsp: Refactor capabilities as an async OnceCell First step in making LSP init asynchronous --- helix-lsp/src/client.rs | 35 ++++++++++++++--------------------- helix-lsp/src/lib.rs | 29 ++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index d0a8183f..87078c69 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -13,7 +13,10 @@ use std::sync::atomic::{AtomicU64, Ordering}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, - sync::mpsc::{channel, UnboundedReceiver, UnboundedSender}, + sync::{ + mpsc::{channel, UnboundedReceiver, UnboundedSender}, + OnceCell, + }, }; #[derive(Debug)] @@ -22,7 +25,7 @@ pub struct Client { _process: Child, server_tx: UnboundedSender, request_counter: AtomicU64, - capabilities: Option, + pub(crate) capabilities: OnceCell, offset_encoding: OffsetEncoding, config: Option, } @@ -57,14 +60,11 @@ impl Client { _process: process, server_tx, request_counter: AtomicU64::new(0), - capabilities: None, + capabilities: OnceCell::new(), offset_encoding: OffsetEncoding::Utf8, config, }; - // TODO: async client.initialize() - // maybe use an arc flag - Ok((client, server_rx)) } @@ -90,7 +90,7 @@ impl Client { pub fn capabilities(&self) -> &lsp::ServerCapabilities { self.capabilities - .as_ref() + .get() .expect("language server not yet initialized!") } @@ -151,7 +151,7 @@ impl Client { } /// Send a RPC notification to the language server. - fn notify( + pub fn notify( &self, params: R::Params, ) -> impl Future> @@ -213,7 +213,7 @@ impl Client { // General messages // ------------------------------------------------------------------------------------------- - pub(crate) async fn initialize(&mut self) -> Result<()> { + pub(crate) async fn initialize(&self) -> Result { // TODO: delay any requests that are triggered prior to initialize let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok()); @@ -281,14 +281,7 @@ impl Client { locale: None, // TODO }; - let response = self.request::(params).await?; - self.capabilities = Some(response.capabilities); - - // next up, notify - self.notify::(lsp::InitializedParams {}) - .await?; - - Ok(()) + self.request::(params).await } pub async fn shutdown(&self) -> Result<()> { @@ -445,7 +438,7 @@ impl Client { ) -> Option>> { // figure out what kind of sync the server supports - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); let sync_capabilities = match capabilities.text_document_sync { Some(lsp::TextDocumentSyncCapability::Kind(kind)) @@ -496,7 +489,7 @@ impl Client { text_document: lsp::TextDocumentIdentifier, text: &Rope, ) -> Result<()> { - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); let include_text = match &capabilities.text_document_sync { Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions { @@ -590,7 +583,7 @@ impl Client { options: lsp::FormattingOptions, work_done_token: Option, ) -> anyhow::Result> { - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_formatting_provider { @@ -618,7 +611,7 @@ impl Client { options: lsp::FormattingOptions, work_done_token: Option, ) -> anyhow::Result> { - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_range_formatting_provider { diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 72606b70..a118239f 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -312,17 +312,40 @@ impl Registry { Entry::Vacant(entry) => { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (mut client, incoming) = Client::start( + let (client, incoming) = Client::start( &config.command, &config.args, serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), id, )?; - // TODO: run this async without blocking - futures_executor::block_on(client.initialize())?; s_incoming.push(UnboundedReceiverStream::new(incoming)); let client = Arc::new(client); + let _client = client.clone(); + let initialize = tokio::spawn(async move { + use futures_util::TryFutureExt; + + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + value.expect("failed to initialize capabilities"); + + // next up, notify + _client + .notify::(lsp::InitializedParams {}) + .await + .unwrap(); + }); + + // TODO: remove this block + futures_executor::block_on(initialize).map_err(|_| anyhow::anyhow!("bail"))?; + entry.insert((id, client.clone())); Ok(client) } From 5a558e0d8e20eb5b5d474e0f27fd51f4c633dd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 16:48:59 +0900 Subject: [PATCH 16/92] lsp: Delay requests & notifications until initialization is complete --- helix-lsp/src/client.rs | 15 ++++--- helix-lsp/src/lib.rs | 11 +++-- helix-lsp/src/transport.rs | 91 +++++++++++++++++++++++++++++++------- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 87078c69..02cd5747 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -9,13 +9,16 @@ use lsp_types as lsp; use serde_json::Value; use std::future::Future; use std::process::Stdio; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, sync::{ mpsc::{channel, UnboundedReceiver, UnboundedSender}, - OnceCell, + Notify, OnceCell, }, }; @@ -31,12 +34,13 @@ pub struct Client { } impl Client { + #[allow(clippy::type_complexity)] pub fn start( cmd: &str, args: &[String], config: Option, id: usize, - ) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> { + ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { let process = Command::new(cmd) .args(args) .stdin(Stdio::piped()) @@ -53,7 +57,8 @@ impl Client { let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout")); let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr")); - let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id); + let (server_rx, server_tx, initialize_notify) = + Transport::start(reader, writer, stderr, id); let client = Self { id, @@ -65,7 +70,7 @@ impl Client { config, }; - Ok((client, server_rx)) + Ok((client, server_rx, initialize_notify)) } pub fn id(&self) -> usize { diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index a118239f..3a761ad0 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -312,7 +312,7 @@ impl Registry { Entry::Vacant(entry) => { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (client, incoming) = Client::start( + let (client, incoming, initialize_notify) = Client::start( &config.command, &config.args, serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), @@ -322,9 +322,9 @@ impl Registry { let client = Arc::new(client); let _client = client.clone(); - let initialize = tokio::spawn(async move { + // Initialize the client asynchronously + tokio::spawn(async move { use futures_util::TryFutureExt; - let value = _client .capabilities .get_or_try_init(|| { @@ -341,10 +341,9 @@ impl Registry { .notify::(lsp::InitializedParams {}) .await .unwrap(); - }); - // TODO: remove this block - futures_executor::block_on(initialize).map_err(|_| anyhow::anyhow!("bail"))?; + initialize_notify.notify_one(); + }); entry.insert((id, client.clone())); Ok(client) diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 9353de20..071c5b93 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,7 +1,7 @@ use crate::{Error, Result}; use anyhow::Context; use jsonrpc_core as jsonrpc; -use log::{debug, error, info, warn}; +use log::{error, info}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -11,7 +11,7 @@ use tokio::{ process::{ChildStderr, ChildStdin, ChildStdout}, sync::{ mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, - Mutex, + Mutex, Notify, }, }; @@ -51,9 +51,11 @@ impl Transport { ) -> ( UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedSender, + Arc, ) { let (client_tx, rx) = unbounded_channel(); let (tx, client_rx) = unbounded_channel(); + let notify = Arc::new(Notify::new()); let transport = Self { id, @@ -64,9 +66,14 @@ impl Transport { tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); tokio::spawn(Self::err(transport.clone(), server_stderr)); - tokio::spawn(Self::send(transport, server_stdin, client_rx)); - - (rx, tx) + tokio::spawn(Self::send( + transport, + server_stdin, + client_rx, + notify.clone(), + )); + + (rx, tx, notify) } async fn recv_server_message( @@ -82,7 +89,8 @@ impl Transport { // debug!("<- header {:?}", buffer); - if header.is_empty() { + if buffer == "\r\n" { + // look for an empty CRLF line break; } @@ -99,7 +107,8 @@ impl Transport { // Workaround: Some non-conformant language servers will output logging and other garbage // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn // the server. Skip such lines and log a warning. - warn!("Failed to parse header: {:?}", header); + + // warn!("Failed to parse header: {:?}", header); } } } @@ -261,15 +270,67 @@ impl Transport { transport: Arc, mut server_stdin: BufWriter, mut client_rx: UnboundedReceiver, + initialize_notify: Arc, ) { - while let Some(msg) = client_rx.recv().await { - match transport - .send_payload_to_server(&mut server_stdin, msg) - .await - { - Ok(_) => {} - Err(err) => { - error!("err: <- {:?}", err); + let mut pending_messages: Vec = Vec::new(); + let mut is_pending = true; + + // Determine if a message is allowed to be sent early + fn is_initialize(payload: &Payload) -> bool { + use lsp_types::{ + notification::{Initialized, Notification}, + request::{Initialize, Request}, + }; + match payload { + Payload::Request { + value: jsonrpc::MethodCall { method, .. }, + .. + } if method == Initialize::METHOD => true, + Payload::Notification(jsonrpc::Notification { method, .. }) + if method == Initialized::METHOD => + { + true + } + _ => false, + } + } + + // TODO: events that use capabilities need to do the right thing + + loop { + tokio::select! { + biased; + _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe + // server successfully initialized + is_pending = false; + // drain the pending queue and send payloads to server + for msg in pending_messages.drain(..) { + log::info!("Draining pending message {:?}", msg); + match transport.send_payload_to_server(&mut server_stdin, msg).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + } + } + msg = client_rx.recv() => { + if let Some(msg) = msg { + if is_pending && !is_initialize(&msg) { + log::info!("Language server not initialized, delaying request"); + pending_messages.push(msg); + } else { + match transport.send_payload_to_server(&mut server_stdin, msg).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + } + } else { + // channel closed + break; + } } } } From 905efe3a48e5ac08a619da95b3d5f845e7e20160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 16:52:12 +0900 Subject: [PATCH 17/92] Improve build error when a new grammar was added --- helix-syntax/build.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helix-syntax/build.rs b/helix-syntax/build.rs index 473646fd..28f85e74 100644 --- a/helix-syntax/build.rs +++ b/helix-syntax/build.rs @@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) { .is_none() { eprintln!( - "The directory {} is empty, did you use 'git clone --recursive'?", + "The directory {} is empty, you probably need to use 'git submodule update --init --recursive'?", dir ); - eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'."); std::process::exit(1); } From d2b9a5d6546a97686338c26b21f886983a93d8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 16:52:27 +0900 Subject: [PATCH 18/92] lsp: Update the julia definition --- languages.toml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index c04435fe..b1c0f479 100644 --- a/languages.toml +++ b/languages.toml @@ -204,7 +204,23 @@ injection-regex = "julia" file-types = ["jl"] roots = [] comment-token = "#" -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);" ] } +language-server = { command = "julia", args = [ + "--startup-file=no", + "--history-file=no", + "--quiet", + "-e", + """ + using LanguageServer; + using Pkg; + import StaticLint; + import SymbolServer; + env_path = dirname(Pkg.Types.Context().env.project_file); + + server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); + server.runlinter = true; + run(server); + """, + ] } indent = { tab-width = 2, unit = " " } [[language]] From 8744f367bdd3fce5e6cc6ee2b5198188598f5170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 18:12:45 +0900 Subject: [PATCH 19/92] wip --- helix-lsp/src/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 02cd5747..ac6ae70a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -148,7 +148,8 @@ impl Client { }) .map_err(|e| Error::Other(e.into()))?; - timeout(Duration::from_secs(2), rx.recv()) + // TODO: specifiable timeout, delay other calls until initialize success + timeout(Duration::from_secs(20), rx.recv()) .await .map_err(|_| Error::Timeout)? // return Timeout .ok_or(Error::StreamClosed)? From c00cf238afe3dbd43327fd74bd8a9ff2dd9c21db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 1 Sep 2021 14:38:47 +0900 Subject: [PATCH 20/92] Simplify textDocument/didClose, we don't need to look up LSP again --- helix-view/src/editor.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 562c3c60..050f2645 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -287,14 +287,9 @@ impl Editor { if close_buffer { // get around borrowck issues - let language_servers = &mut self.language_servers; let doc = &self.documents[view.doc]; - let language_server = doc - .language - .as_ref() - .and_then(|language| language_servers.get(language).ok()); - if let Some(language_server) = language_server { + if let Some(language_server) = doc.language_server() { tokio::spawn(language_server.text_document_did_close(doc.identifier())); } self.documents.remove(view.doc); From 184637c55acca49380372ca118f13b3390bcb003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 1 Sep 2021 14:49:48 +0900 Subject: [PATCH 21/92] lsp: refactor format so we stop cloning the language_server --- helix-lsp/src/client.rs | 14 +++++++++----- helix-view/src/document.rs | 23 +++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index ac6ae70a..fdff553f 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -583,19 +583,19 @@ impl Client { // formatting - pub async fn text_document_formatting( + pub fn text_document_formatting( &self, text_document: lsp::TextDocumentIdentifier, options: lsp::FormattingOptions, work_done_token: Option, - ) -> anyhow::Result> { + ) -> Option>>> { let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_formatting_provider { Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), // None | Some(false) - _ => return Ok(Vec::new()), + _ => return None, }; // TODO: return err::unavailable so we can fall back to tree sitter formatting @@ -605,9 +605,13 @@ impl Client { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token }, }; - let response = self.request::(params).await?; + let request = self.call::(params); - Ok(response.unwrap_or_default()) + Some(async move { + let json = request.await?; + let response: Vec = serde_json::from_value(json)?; + Ok(response) + }) } pub async fn text_document_range_formatting( diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b2c02927..a27be8e6 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -386,21 +386,24 @@ impl Document { /// If supported, returns the changes that should be applied to this document in order /// to format it nicely. pub fn format(&self) -> Option + 'static> { - if let Some(language_server) = self.language_server.clone() { + if let Some(language_server) = self.language_server() { let text = self.text.clone(); - let id = self.identifier(); + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions::default(), + None, + )?; + let fut = async move { - let edits = language_server - .text_document_formatting(id, lsp::FormattingOptions::default(), None) - .await - .unwrap_or_else(|e| { - log::warn!("LSP formatting failed: {}", e); - Default::default() - }); + let edits = request.await.unwrap_or_else(|e| { + log::warn!("LSP formatting failed: {}", e); + Default::default() + }); LspFormatting { doc: text, edits, - offset_encoding: language_server.offset_encoding(), + offset_encoding, } }; Some(fut) From 800d79b584cd09020488b8a614e5214b929d8f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 1 Sep 2021 14:54:11 +0900 Subject: [PATCH 22/92] ls: Refactor textDocument/didSave in a similar vein --- helix-lsp/src/client.rs | 19 ++++++++++--------- helix-view/src/document.rs | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index fdff553f..b8fbfddb 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -490,11 +490,11 @@ impl Client { // will_save / will_save_wait_until - pub async fn text_document_did_save( + pub fn text_document_did_save( &self, text_document: lsp::TextDocumentIdentifier, text: &Rope, - ) -> Result<()> { + ) -> Option>> { let capabilities = self.capabilities.get().unwrap(); let include_text = match &capabilities.text_document_sync { @@ -507,17 +507,18 @@ impl Client { include_text, }) => include_text.unwrap_or(false), // Supported(false) - _ => return Ok(()), + _ => return None, }, // unsupported - _ => return Ok(()), + _ => return None, }; - self.notify::(lsp::DidSaveTextDocumentParams { - text_document, - text: include_text.then(|| text.into()), - }) - .await + Some(self.notify::( + lsp::DidSaveTextDocumentParams { + text_document, + text: include_text.then(|| text.into()), + }, + )) } pub fn completion( diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index a27be8e6..5677eb44 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -471,10 +471,10 @@ impl Document { let mut file = File::create(path).await?; to_writer(&mut file, encoding, &text).await?; - if let Some(language_server) = language_server { - language_server - .text_document_did_save(identifier, &text) - .await?; + if let Some(notification) = language_server.and_then(|language_server| { + language_server.text_document_did_save(identifier, &text) + }) { + notification.await?; } Ok(()) From 10b690b5bd5d3e9ee477782ebfe3f6ff8d11cb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 10:49:23 +0900 Subject: [PATCH 23/92] Drop some &mut bounds where & would have sufficed --- helix-term/src/job.rs | 4 ++-- helix-view/src/document.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 2ac41926..4fa38174 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -61,7 +61,7 @@ impl Jobs { } pub fn handle_callback( - &mut self, + &self, editor: &mut Editor, compositor: &mut Compositor, call: anyhow::Result>, @@ -84,7 +84,7 @@ impl Jobs { } } - pub fn add(&mut self, j: Job) { + pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); } else { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 5677eb44..71f6680c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -649,7 +649,7 @@ impl Document { // } // emit lsp notification - if let Some(language_server) = &self.language_server { + if let Some(language_server) = self.language_server() { let notify = language_server.text_document_did_change( self.versioned_identifier(), &old_doc, From 48fd4843fc4a28bfd05ea01ef0d10f4ea816db20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 11:10:00 +0900 Subject: [PATCH 24/92] lsp: Outdated comment --- helix-lsp/src/lib.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 3a761ad0..e10c107b 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -437,32 +437,6 @@ impl LspProgressMap { } } -// REGISTRY = HashMap>> -// spawn one server per language type, need to spawn one per workspace if server doesn't support -// workspaces -// -// could also be a client per root dir -// -// storing a copy of Option>> on Document would make the LSP client easily -// accessible during edit/save callbacks -// -// the event loop needs to process all incoming streams, maybe we can just have that be a separate -// task that's continually running and store the state on the client, then use read lock to -// retrieve data during render -// -> PROBLEM: how do you trigger an update on the editor side when data updates? -// -// -> The data updates should pull all events until we run out so we don't frequently re-render -// -// -// v2: -// -// there should be a registry of lsp clients, one per language type (or workspace). -// the clients should lazy init on first access -// the client.initialize() should be called async and we buffer any requests until that completes -// there needs to be a way to process incoming lsp messages from all clients. -// -> notifications need to be dispatched to wherever -// -> requests need to generate a reply and travel back to the same lsp! - #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; From 63e191ea3b2ce116c39a446b8fab10a360fd8a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 11:19:32 +0900 Subject: [PATCH 25/92] lsp: Simplify lookup under method call --- helix-term/src/application.rs | 65 ++++++++++------------------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1fcca681..8241ce3a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -4,7 +4,7 @@ use helix_view::{theme, Editor}; use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; -use log::error; +use log::{error, warn}; use std::{ io::{stdout, Write}, @@ -429,10 +429,27 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { + let language_server = match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + let call = match MethodCall::parse(&method, params) { Some(call) => call, None => { error!("Method not found {}", method); + // language_server.reply( + // call.id, + // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) + // Err(helix_lsp::jsonrpc::Error { + // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, + // message: "Method not found".to_string(), + // data: None, + // }), + // ); return; } }; @@ -445,53 +462,9 @@ impl Application { if spinner.is_stopped() { spinner.start(); } - - let doc = self.editor.documents().find(|doc| { - doc.language_server() - .map(|server| server.id() == server_id) - .unwrap_or_default() - }); - match doc { - Some(doc) => { - // it's ok to unwrap, we check for the language server before - let server = doc.language_server().unwrap(); - tokio::spawn(server.reply(id, Ok(serde_json::Value::Null))); - } - None => { - if let Some(server) = - self.editor.language_servers.get_by_id(server_id) - { - log::warn!( - "missing document with language server id `{}`", - server_id - ); - tokio::spawn(server.reply( - id, - Err(helix_lsp::jsonrpc::Error { - code: helix_lsp::jsonrpc::ErrorCode::InternalError, - message: "document missing".to_string(), - data: None, - }), - )); - } else { - log::warn!( - "can't find language server with id `{}`", - server_id - ); - } - } - } + tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); } } - // self.language_server.reply( - // call.id, - // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) - // Err(helix_lsp::jsonrpc::Error { - // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, - // message: "Method not found".to_string(), - // data: None, - // }), - // ); } e => unreachable!("{:?}", e), } From dc7799b980826ffe33ed635968def79daf20bd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 11:28:40 +0900 Subject: [PATCH 26/92] lsp: Refactor code that could use document_by_path_mut --- helix-term/src/application.rs | 11 +++-------- helix-view/src/editor.rs | 5 +++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8241ce3a..d3b65a4f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -276,15 +276,10 @@ impl Application { match notification { Notification::PublishDiagnostics(params) => { - let path = Some(params.uri.to_file_path().unwrap()); + let path = params.uri.to_file_path().unwrap(); + let doc = self.editor.document_by_path_mut(&path); - let doc = self - .editor - .documents - .iter_mut() - .find(|(_, doc)| doc.path() == path.as_ref()); - - if let Some((_, doc)) = doc { + if let Some(doc) = doc { let text = doc.text(); let diagnostics = params diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 050f2645..0d914e45 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -340,6 +340,11 @@ impl Editor { .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) } + pub fn document_by_path_mut>(&mut self, path: P) -> Option<&mut Document> { + self.documents_mut() + .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) + } + pub fn cursor(&self) -> (Option, CursorKind) { let view = view!(self); let doc = &self.documents[view.doc]; From 59ed1c8c78ab996273bcc910c11724bc8402f711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 12:52:32 +0900 Subject: [PATCH 27/92] Simplify documents & documents_mut() --- helix-view/src/editor.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 0d914e45..c8abd5b5 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -319,20 +319,24 @@ impl Editor { view.ensure_cursor_in_view(doc, self.config.scrolloff) } + #[inline] pub fn document(&self, id: DocumentId) -> Option<&Document> { self.documents.get(id) } + #[inline] pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> { self.documents.get_mut(id) } + #[inline] pub fn documents(&self) -> impl Iterator { - self.documents.iter().map(|(_id, doc)| doc) + self.documents.values() } + #[inline] pub fn documents_mut(&mut self) -> impl Iterator { - self.documents.iter_mut().map(|(_id, doc)| doc) + self.documents.values_mut() } pub fn document_by_path>(&self, path: P) -> Option<&Document> { From 2793ff383228403b1ebaf2a29c870a13ee76075a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 13:52:12 +0900 Subject: [PATCH 28/92] lsp: SyncKind::Full: we need to send the whole document on each change --- helix-lsp/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index b8fbfddb..27e4697c 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -462,7 +462,7 @@ impl Client { // range = None -> whole document range: None, //Some(Range) range_length: None, // u64 apparently deprecated - text: "".to_string(), + text: new_text.to_string(), }] } lsp::TextDocumentSyncKind::Incremental => { From 46f3c69f06cc55f36bcc6244a9f96c2481836dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 13:55:08 +0900 Subject: [PATCH 29/92] lsp: Don't send notifications until initialize completes Then send open events for all documents with the LSP attached. --- helix-lsp/src/lib.rs | 98 +++++++++++++++++------------------ helix-lsp/src/transport.rs | 29 ++++++++++- helix-term/src/application.rs | 31 +++++++++++ helix-view/src/editor.rs | 5 +- 4 files changed, 111 insertions(+), 52 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index e10c107b..7357c885 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -226,6 +226,8 @@ impl MethodCall { #[derive(Debug, PartialEq, Clone)] pub enum Notification { + // we inject this notification to signal the LSP is ready + Initialized, PublishDiagnostics(lsp::PublishDiagnosticsParams), ShowMessage(lsp::ShowMessageParams), LogMessage(lsp::LogMessageParams), @@ -237,6 +239,7 @@ impl Notification { use lsp::notification::Notification as _; let notification = match method { + lsp::notification::Initialized::METHOD => Self::Initialized, lsp::notification::PublishDiagnostics::METHOD => { let params: lsp::PublishDiagnosticsParams = params .parse() @@ -294,7 +297,7 @@ impl Registry { } } - pub fn get_by_id(&mut self, id: usize) -> Option<&Client> { + pub fn get_by_id(&self, id: usize) -> Option<&Client> { self.inner .values() .find(|(client_id, _)| client_id == &id) @@ -302,55 +305,52 @@ impl Registry { } pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result> { - if let Some(config) = &language_config.language_server { - // avoid borrow issues - let inner = &mut self.inner; - let s_incoming = &mut self.incoming; - - match inner.entry(language_config.scope.clone()) { - Entry::Occupied(entry) => Ok(entry.get().1.clone()), - Entry::Vacant(entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (client, incoming, initialize_notify) = Client::start( - &config.command, - &config.args, - serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), - id, - )?; - s_incoming.push(UnboundedReceiverStream::new(incoming)); - let client = Arc::new(client); - - let _client = client.clone(); - // Initialize the client asynchronously - tokio::spawn(async move { - use futures_util::TryFutureExt; - let value = _client - .capabilities - .get_or_try_init(|| { - _client - .initialize() - .map_ok(|response| response.capabilities) - }) - .await; - - value.expect("failed to initialize capabilities"); - - // next up, notify - _client - .notify::(lsp::InitializedParams {}) - .await - .unwrap(); - - initialize_notify.notify_one(); - }); - - entry.insert((id, client.clone())); - Ok(client) - } + let config = match &language_config.language_server { + Some(config) => config, + None => return Err(Error::LspNotDefined), + }; + + match self.inner.entry(language_config.scope.clone()) { + Entry::Occupied(entry) => Ok(entry.get().1.clone()), + Entry::Vacant(entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + let (client, incoming, initialize_notify) = Client::start( + &config.command, + &config.args, + serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), + id, + )?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + let client = Arc::new(client); + + // Initialize the client asynchronously + let _client = client.clone(); + tokio::spawn(async move { + use futures_util::TryFutureExt; + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + value.expect("failed to initialize capabilities"); + + // next up, notify + _client + .notify::(lsp::InitializedParams {}) + .await + .unwrap(); + + initialize_notify.notify_one(); + }); + + entry.insert((id, client.clone())); + Ok(client) } - } else { - Err(Error::LspNotDefined) } } diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 071c5b93..cf7e66a8 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -64,11 +64,16 @@ impl Transport { let transport = Arc::new(transport); - tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); + tokio::spawn(Self::recv( + transport.clone(), + server_stdout, + client_tx.clone(), + )); tokio::spawn(Self::err(transport.clone(), server_stderr)); tokio::spawn(Self::send( transport, server_stdin, + client_tx, client_rx, notify.clone(), )); @@ -269,6 +274,7 @@ impl Transport { async fn send( transport: Arc, mut server_stdin: BufWriter, + mut client_tx: UnboundedSender<(usize, jsonrpc::Call)>, mut client_rx: UnboundedReceiver, initialize_notify: Arc, ) { @@ -303,6 +309,22 @@ impl Transport { _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe // server successfully initialized is_pending = false; + + use lsp_types::notification::Notification; + // Hack: inject an initialized notification so we trigger code that needs to happen after init + let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification { + jsonrpc: None, + + method: lsp_types::notification::Initialized::METHOD.to_string(), + params: jsonrpc::Params::None, + })); + match transport.process_server_message(&mut client_tx, notification).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + // drain the pending queue and send payloads to server for msg in pending_messages.drain(..) { log::info!("Draining pending message {:?}", msg); @@ -317,6 +339,11 @@ impl Transport { msg = client_rx.recv() => { if let Some(msg) = msg { if is_pending && !is_initialize(&msg) { + // ignore notifications + if let Payload::Notification(_) = msg { + continue; + } + log::info!("Language server not initialized, delaying request"); pending_messages.push(msg); } else { diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index d3b65a4f..e21c5504 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -275,6 +275,37 @@ impl Application { }; match notification { + Notification::Initialized => { + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + + let docs = self.editor.documents().filter(|doc| { + doc.language_server().map(|server| server.id()) == Some(server_id) + }); + + // trigger textDocument/didOpen for docs that are already open + for doc in docs { + // TODO: extract and share with editor.open + let language_id = doc + .language() + .and_then(|s| s.split('.').last()) // source.rust + .map(ToOwned::to_owned) + .unwrap_or_default(); + + tokio::spawn(language_server.text_document_did_open( + doc.url().unwrap(), + doc.version(), + doc.text(), + language_id, + )); + } + } Notification::PublishDiagnostics(params) => { let path = params.uri.to_file_path().unwrap(); let doc = self.editor.document_by_path_mut(&path); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c8abd5b5..3d2d4a87 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -255,20 +255,21 @@ impl Editor { .and_then(|language| self.language_servers.get(language).ok()); if let Some(language_server) = language_server { - doc.set_language_server(Some(language_server.clone())); - let language_id = doc .language() .and_then(|s| s.split('.').last()) // source.rust .map(ToOwned::to_owned) .unwrap_or_default(); + // TODO: this now races with on_init code if the init happens too quickly tokio::spawn(language_server.text_document_did_open( doc.url().unwrap(), doc.version(), doc.text(), language_id, )); + + doc.set_language_server(Some(language_server)); } let id = self.documents.insert(doc); From 37606bad47fe0e197cb34fc7d52856597c32ce33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 13:55:55 +0900 Subject: [PATCH 30/92] lsp: doc.language_server() is None until initialize completes --- helix-lsp/src/client.rs | 4 ++++ helix-view/src/document.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 27e4697c..f2bb0059 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -93,6 +93,10 @@ impl Client { } } + pub fn is_initialized(&self) -> bool { + self.capabilities.get().is_some() + } + pub fn capabilities(&self) -> &lsp::ServerCapabilities { self.capabilities .get() diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 71f6680c..362a433e 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -798,9 +798,18 @@ impl Document { self.version } - #[inline] pub fn language_server(&self) -> Option<&helix_lsp::Client> { - self.language_server.as_deref() + let server = self.language_server.as_deref(); + let initialized = server + .map(|server| server.is_initialized()) + .unwrap_or(false); + + // only resolve language_server if it's initialized + if initialized { + server + } else { + None + } } #[inline] From a6108baec9455f223211b458e2328087e7fa3280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 14:58:57 +0900 Subject: [PATCH 31/92] Improve grammar definitions --- helix-syntax/languages/tree-sitter-julia | 2 +- runtime/queries/julia/highlights.scm | 82 +++++++++++++----------- runtime/queries/latex/highlights.scm | 4 +- runtime/queries/lua/highlights.scm | 8 +-- runtime/queries/ocaml/highlights.scm | 4 +- runtime/queries/rust/highlights.scm | 2 +- runtime/queries/yaml/highlights.scm | 2 +- 7 files changed, 54 insertions(+), 50 deletions(-) diff --git a/helix-syntax/languages/tree-sitter-julia b/helix-syntax/languages/tree-sitter-julia index 0ba7a24b..12ea5972 160000 --- a/helix-syntax/languages/tree-sitter-julia +++ b/helix-syntax/languages/tree-sitter-julia @@ -1 +1 @@ -Subproject commit 0ba7a24b062b671263ae08e707e9e94383b25bb7 +Subproject commit 12ea597262125fc22fd2e91aa953ac69b19c26ca diff --git a/runtime/queries/julia/highlights.scm b/runtime/queries/julia/highlights.scm index a53dabe5..7b7d426c 100644 --- a/runtime/queries/julia/highlights.scm +++ b/runtime/queries/julia/highlights.scm @@ -1,9 +1,3 @@ -(identifier) @variable -;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) -;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables - ;(match? @type "^[A-Z][^_]")) -((identifier) @constant - (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) [ (triple_string) @@ -28,43 +22,43 @@ (call_expression (identifier) @function) (call_expression - (field_expression (identifier) @method .)) + (field_expression (identifier) @function.method .)) (broadcast_call_expression (identifier) @function) (broadcast_call_expression - (field_expression (identifier) @method .)) + (field_expression (identifier) @function.method .)) (parameter_list - (identifier) @parameter) + (identifier) @variable.parameter) (parameter_list (optional_parameter . - (identifier) @parameter)) + (identifier) @variable.parameter)) (typed_parameter - (identifier) @parameter + (identifier) @variable.parameter (identifier) @type) (type_parameter_list (identifier) @type) (typed_parameter - (identifier) @parameter + (identifier) @variable.parameter (parameterized_identifier) @type) (function_expression - . (identifier) @parameter) -(spread_parameter) @parameter + . (identifier) @variable.parameter) +(spread_parameter) @variable.parameter (spread_parameter - (identifier) @parameter) + (identifier) @variable.parameter) (named_argument - . (identifier) @parameter) + . (identifier) @variable.parameter) (argument_list (typed_expression - (identifier) @parameter + (identifier) @variable.parameter (identifier) @type)) (argument_list (typed_expression - (identifier) @parameter + (identifier) @variable.parameter (parameterized_identifier) @type)) ;; Symbol expressions (:my-wanna-be-lisp-keyword) (quote_expression - (identifier)) @symbol + (identifier)) @string.special.symbol ;; Parsing error! foo (::Type) get's parsed as two quote expressions (argument_list @@ -76,7 +70,7 @@ (identifier) @type) (parameterized_identifier (_)) @type (argument_list - (typed_expression . (identifier) @parameter)) + (typed_expression . (identifier) @variable.parameter)) (typed_expression (identifier) @type .) @@ -113,13 +107,13 @@ "end" @keyword (if_statement - ["if" "end"] @conditional) + ["if" "end"] @keyword.control.conditional) (elseif_clause - ["elseif"] @conditional) + ["elseif"] @keyword.control.conditional) (else_clause - ["else"] @conditional) + ["else"] @keyword.control.conditional) (ternary_expression - ["?" ":"] @conditional) + ["?" ":"] @keyword.control.conditional) (function_definition ["function" "end"] @keyword.function) @@ -134,47 +128,57 @@ "type" ] @keyword -((identifier) @keyword (#any-of? @keyword "global" "local")) +((identifier) @keyword (match? @keyword "global|local")) (compound_expression ["begin" "end"] @keyword) (try_statement - ["try" "end" ] @exception) + ["try" "end" ] @keyword.control.exception) (finally_clause - "finally" @exception) + "finally" @keyword.control.exception) (catch_clause - "catch" @exception) + "catch" @keyword.control.exception) (quote_statement ["quote" "end"] @keyword) (let_statement ["let" "end"] @keyword) (for_statement - ["for" "end"] @repeat) + ["for" "end"] @keyword.control.repeat) (while_statement - ["while" "end"] @repeat) -(break_statement) @repeat -(continue_statement) @repeat + ["while" "end"] @keyword.control.repeat) +(break_statement) @keyword.control.repeat +(continue_statement) @keyword.control.repeat (for_binding - "in" @repeat) + "in" @keyword.control.repeat) (for_clause - "for" @repeat) + "for" @keyword.control.repeat) (do_clause ["do" "end"] @keyword) (export_statement - ["export"] @include) + ["export"] @keyword.control.import) [ "using" "module" "import" -] @include +] @keyword.control.import -((identifier) @include (#eq? @include "baremodule")) +((identifier) @keyword.control.import (#eq? @keyword.control.import "baremodule")) (((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$")) -(((identifier) @boolean) (eq? @boolean "true")) -(((identifier) @boolean) (eq? @boolean "false")) +(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true")) +(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false")) + ["::" ":" "." "," "..." "!"] @punctuation.delimiter ["[" "]" "(" ")" "{" "}"] @punctuation.bracket + +["="] @operator + +(identifier) @variable +;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) +;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables + ;(match? @type "^[A-Z][^_]")) +((identifier) @constant + (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm index cd04a62c..f045c82d 100644 --- a/runtime/queries/latex/highlights.scm +++ b/runtime/queries/latex/highlights.scm @@ -259,7 +259,7 @@ (comment) @comment -(bracket_group) @parameter +(bracket_group) @variable.parameter [(math_operator) "="] @operator @@ -312,7 +312,7 @@ key: (word) @text.reference) (key_val_pair - key: (_) @parameter + key: (_) @variable.parameter value: (_)) ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm index 8e27a39a..4e68cd66 100644 --- a/runtime/queries/lua/highlights.scm +++ b/runtime/queries/lua/highlights.scm @@ -108,7 +108,7 @@ [ (false) (true) -] @boolean +] @constant.builtin.boolean (nil) @constant.builtin (spread) @constant ;; "..." ((identifier) @constant @@ -116,7 +116,7 @@ ;; Parameters (parameters - (identifier) @parameter) + (identifier) @variable.parameter) ; ;; Functions (function [(function_name) (identifier)] @function) @@ -139,8 +139,8 @@ (function_call [ - ((identifier) @variable (method) @method) - ((_) (method) @method) + ((identifier) @variable (method) @function.method) + ((_) (method) @function.method) (identifier) @function (field_expression (property_identifier) @function) ] diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 093b3cce..3bb42604 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -25,12 +25,12 @@ (external (value_name) @function) -(method_name) @method +(method_name) @function.method ; Variables ;---------- -(value_pattern) @parameter +(value_pattern) @variable.parameter ; Application ;------------ diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 6b14d74d..c76d6adf 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -17,7 +17,7 @@ (escape_sequence) @escape (primitive_type) @type.builtin -(boolean_literal) @constant.builtin +(boolean_literal) @constant.builtin.boolean [ (integer_literal) (float_literal) diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm index 4ebb4440..2955a4ce 100644 --- a/runtime/queries/yaml/highlights.scm +++ b/runtime/queries/yaml/highlights.scm @@ -1,6 +1,6 @@ (block_mapping_pair key: (_) @property) (flow_mapping (_ key: (_) @property)) -(boolean_scalar) @boolean +(boolean_scalar) @constant.builtin.boolean (null_scalar) @constant.builtin (double_quote_scalar) @string (single_quote_scalar) @string From 585e3ce83066bfe0235598660d32a6e171cc60cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 15:11:27 +0900 Subject: [PATCH 32/92] fix: tree-sitter-scopes would infinitely loop --- helix-core/src/indent.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index f5f36aca..55802059 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -316,8 +316,12 @@ pub fn suggested_indent_for_pos( pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> { let mut scopes = Vec::new(); if let Some(syntax) = syntax { - let byte_start = text.char_to_byte(pos); - let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) { + let pos = text.char_to_byte(pos); + let mut node = match syntax + .tree() + .root_node() + .descendant_for_byte_range(pos, pos) + { Some(node) => node, None => return scopes, }; @@ -325,7 +329,8 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<& scopes.push(node.kind()); while let Some(parent) = node.parent() { - scopes.push(parent.kind()) + scopes.push(parent.kind()); + node = parent; } } From ade1a453efe8bc6d4c5e775ba39da149ef01bb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 15:11:45 +0900 Subject: [PATCH 33/92] syntax: Improve go highlights --- runtime/queries/go/highlights.scm | 39 +++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 224c8b78..6e5d65a0 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -17,9 +17,18 @@ ; Identifiers +((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$")) +(const_spec + name: (identifier) @constant) + (type_identifier) @type (field_identifier) @property (identifier) @variable +(package_identifier) @variable + +(parameter_declaration (identifier) @variable.parameter) +(variadic_parameter_declaration (identifier) @variable.parameter) + ; Operators @@ -79,10 +88,8 @@ "go" "goto" "if" - "import" "interface" "map" - "package" "range" "return" "select" @@ -92,6 +99,29 @@ "var" ] @keyword +[ + "import" + "package" +] @keyword.control.import + +; Delimiters + +[ + ":" + "." + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + ; Literals [ @@ -111,7 +141,8 @@ [ (true) (false) - (nil) -] @constant.builtin +] @constant.builtin.boolean + +(nil) @constant.builtin (comment) @comment From 64099af3f1f5957de8f4203c665063d02ba4bac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 2 Sep 2021 16:07:21 +0900 Subject: [PATCH 34/92] Don't panic on save if language_server isn't initialized --- helix-view/src/document.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 362a433e..6de60995 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -471,10 +471,15 @@ impl Document { let mut file = File::create(path).await?; to_writer(&mut file, encoding, &text).await?; - if let Some(notification) = language_server.and_then(|language_server| { - language_server.text_document_did_save(identifier, &text) - }) { - notification.await?; + if let Some(language_server) = language_server { + if language_server.is_initialized() { + return Ok(()); + } + if let Some(notification) = + language_server.text_document_did_save(identifier, &text) + { + notification.await?; + } } Ok(()) From 9b9c3c77f88af1ff798739aaa3303d563bbe8174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 3 Sep 2021 10:39:30 +0900 Subject: [PATCH 35/92] runtime: Query improvements --- runtime/queries/c/highlights.scm | 2 +- runtime/queries/javascript/highlights.scm | 2 +- runtime/queries/lua/highlights.scm | 8 ++++---- runtime/queries/ruby/highlights.scm | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/runtime/queries/c/highlights.scm b/runtime/queries/c/highlights.scm index 258e07e7..2c42710f 100644 --- a/runtime/queries/c/highlights.scm +++ b/runtime/queries/c/highlights.scm @@ -61,7 +61,7 @@ (null) @constant (number_literal) @number -(char_literal) @number +(char_literal) @string (call_expression function: (identifier) @function) diff --git a/runtime/queries/javascript/highlights.scm b/runtime/queries/javascript/highlights.scm index a18c38d9..e29829bf 100644 --- a/runtime/queries/javascript/highlights.scm +++ b/runtime/queries/javascript/highlights.scm @@ -87,7 +87,7 @@ (template_string) ] @string -(regex) @string.special +(regex) @string.regexp (number) @number ; Tokens diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm index 4e68cd66..756ab3b7 100644 --- a/runtime/queries/lua/highlights.scm +++ b/runtime/queries/lua/highlights.scm @@ -23,27 +23,27 @@ "for" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (for_in_statement [ "for" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (while_statement [ "while" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (repeat_statement [ "repeat" "until" -] @keyword.control.loop) +] @keyword.control.repeat) (do_statement [ diff --git a/runtime/queries/ruby/highlights.scm b/runtime/queries/ruby/highlights.scm index 7f296f3b..8617d6f0 100644 --- a/runtime/queries/ruby/highlights.scm +++ b/runtime/queries/ruby/highlights.scm @@ -100,7 +100,7 @@ (bare_symbol) ] @string.special.symbol -(regex) @string.special.regex +(regex) @string.regexp (escape_sequence) @escape [ From be81f40df8c901f506708a2ce4ff10632fa1d64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 11:32:50 +0900 Subject: [PATCH 36/92] lsp: This doesn't need to be a mutable reference --- helix-lsp/src/transport.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index cf7e66a8..6e28094d 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -274,7 +274,7 @@ impl Transport { async fn send( transport: Arc, mut server_stdin: BufWriter, - mut client_tx: UnboundedSender<(usize, jsonrpc::Call)>, + client_tx: UnboundedSender<(usize, jsonrpc::Call)>, mut client_rx: UnboundedReceiver, initialize_notify: Arc, ) { @@ -318,7 +318,7 @@ impl Transport { method: lsp_types::notification::Initialized::METHOD.to_string(), params: jsonrpc::Params::None, })); - match transport.process_server_message(&mut client_tx, notification).await { + match transport.process_server_message(&client_tx, notification).await { Ok(_) => {} Err(err) => { error!("err: <- {:?}", err); From d85a8adb2747e25589de82d821cd0c4e2dd244f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 14:33:02 +0900 Subject: [PATCH 37/92] Improve highlighting scopes --- runtime/queries/haskell/highlights.scm | 14 ++++++++------ runtime/queries/lua/highlights.scm | 2 +- runtime/queries/ocaml/highlights.scm | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm index ecaa2d2c..dada80b6 100644 --- a/runtime/queries/haskell/highlights.scm +++ b/runtime/queries/haskell/highlights.scm @@ -2,19 +2,19 @@ (operator) @operator (exp_name (constructor) @constructor) (constructor_operator) @operator -(module) @module_name +(module) @namespace (type) @type (type) @class (constructor) @constructor (pragma) @pragma (comment) @comment (signature name: (variable) @fun_type_name) -(function name: (variable) @fun_name) +(function name: (variable) @function) (constraint class: (class_name (type)) @class) (class (class_head class: (class_name (type)) @class)) (instance (instance_head class: (class_name (type)) @class)) -(integer) @literal -(exp_literal (float)) @literal +(integer) @number +(exp_literal (float)) @number (char) @literal (con_unit) @literal (con_list) @literal @@ -39,5 +39,7 @@ "do" @keyword "mdo" @keyword "rec" @keyword -"(" @paren -")" @paren +[ + "(" + ")" +] @punctuation.bracket diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm index 756ab3b7..40c2be70 100644 --- a/runtime/queries/lua/highlights.scm +++ b/runtime/queries/lua/highlights.scm @@ -65,7 +65,7 @@ "not" "and" "or" -] @keyword.operator +] @operator [ "=" diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 3bb42604..5554574d 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -60,7 +60,7 @@ [(number) (signed_number)] @number -(character) @character +(character) @constant.builtin.character (string) @string @@ -92,7 +92,7 @@ ["include" "open"] @include -["for" "to" "downto" "while" "do" "done"] @keyword.control.loop +["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat ; Macros ;------- From 2bef245b7a1ae11f023d3e3aa82df250a3488d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 14:33:19 +0900 Subject: [PATCH 38/92] At least partly highlight tsx --- languages.toml | 11 +++++++++++ runtime/queries/tsx/highlights.scm | 1 + 2 files changed, 12 insertions(+) create mode 100644 runtime/queries/tsx/highlights.scm diff --git a/languages.toml b/languages.toml index b1c0f479..c1ddbea2 100644 --- a/languages.toml +++ b/languages.toml @@ -116,6 +116,17 @@ roots = [] language-server = { command = "typescript-language-server", args = ["--stdio"] } indent = { tab-width = 2, unit = " " } +[[language]] +name = "tsx" +scope = "source.tsx" +injection-regex = "^(tsx)$" # |typescript +file-types = ["tsx"] +roots = [] +# TODO: highlights-jsx, highlights-params + +language-server = { command = "typescript-language-server", args = ["--stdio"] } +indent = { tab-width = 2, unit = " " } + [[language]] name = "css" scope = "source.css" diff --git a/runtime/queries/tsx/highlights.scm b/runtime/queries/tsx/highlights.scm new file mode 100644 index 00000000..1b61e36d --- /dev/null +++ b/runtime/queries/tsx/highlights.scm @@ -0,0 +1 @@ +; inherits: typescript From 4ac29434cb99b517dba3752a287c0edfe8679e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 6 Sep 2021 18:13:52 +0900 Subject: [PATCH 39/92] syntax: Add go & rust locals, improve tree-sitter error message --- helix-core/src/syntax.rs | 8 ++++++-- runtime/queries/go/highlights.scm | 6 +++--- runtime/queries/go/locals.scm | 30 ++++++++++++++++++++++++++++++ runtime/queries/rust/locals.scm | 17 +++++++++++++++++ theme.toml | 2 +- 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 runtime/queries/go/locals.scm create mode 100644 runtime/queries/rust/locals.scm diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a7a5d022..1afe0e25 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -144,8 +144,12 @@ impl LanguageConfiguration { &highlights_query, &injections_query, &locals_query, - ) - .unwrap(); // TODO: no unwrap + ); + + let config = match config { + Ok(config) => config, + Err(err) => panic!("{}", err), + }; // TODO: avoid panic config.configure(scopes); Some(Arc::new(config)) } diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 6e5d65a0..3129c4b2 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -21,14 +21,14 @@ (const_spec name: (identifier) @constant) +(parameter_declaration (identifier) @variable.parameter) +(variadic_parameter_declaration (identifier) @variable.parameter) + (type_identifier) @type (field_identifier) @property (identifier) @variable (package_identifier) @variable -(parameter_declaration (identifier) @variable.parameter) -(variadic_parameter_declaration (identifier) @variable.parameter) - ; Operators diff --git a/runtime/queries/go/locals.scm b/runtime/queries/go/locals.scm new file mode 100644 index 00000000..d240e2b7 --- /dev/null +++ b/runtime/queries/go/locals.scm @@ -0,0 +1,30 @@ +; Scopes + +(block) @local.scope + +; Definitions + +(parameter_declaration (identifier) @local.definition) +(variadic_parameter_declaration (identifier) @local.definition) + +(short_var_declaration + left: (expression_list + (identifier) @local.definition)) + +(var_spec + name: (identifier) @local.definition) + +(for_statement + (range_clause + left: (expression_list + (identifier) @local.definition))) + +(const_declaration + (const_spec + name: (identifier) @local.definition)) + +; References + +(identifier) @local.reference +(field_identifier) @local.reference + diff --git a/runtime/queries/rust/locals.scm b/runtime/queries/rust/locals.scm new file mode 100644 index 00000000..6428f9b4 --- /dev/null +++ b/runtime/queries/rust/locals.scm @@ -0,0 +1,17 @@ +; Scopes + +(block) @local.scope + +; Definitions + +(parameter + (identifier) @local.definition) + +(let_declaration + pattern: (identifier) @local.definition) + +(closure_parameters (identifier)) @local.definition + +; References +(identifier) @local.reference + diff --git a/theme.toml b/theme.toml index 3166b2d6..867a0d2c 100644 --- a/theme.toml +++ b/theme.toml @@ -9,7 +9,7 @@ special = "honey" property = "white" variable = "lavender" # variable = "almond" # TODO: metavariables only -"variable.parameter" = "lavender" +"variable.parameter" = { fg = "lavender", modifiers = ["underlined"] } "variable.builtin" = "mint" type = "white" "type.builtin" = "white" # TODO: distinguish? From 4320821fa464ccdb56106ddf333dc8534ab1ef21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 09:15:11 +0900 Subject: [PATCH 40/92] build(deps): bump thiserror from 1.0.28 to 1.0.29 (#720) Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.28 to 1.0.29. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.28...1.0.29) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b2492b8..249eecd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,18 +930,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" dependencies = [ "proc-macro2", "quote", From 3fc4e9ff58d1ba5f4899b11787245fbaf31bc33d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 09:15:21 +0900 Subject: [PATCH 41/92] build(deps): bump cc from 1.0.69 to 1.0.70 (#721) Bumps [cc](https://github.com/alexcrichton/cc-rs) from 1.0.69 to 1.0.70. - [Release notes](https://github.com/alexcrichton/cc-rs/releases) - [Commits](https://github.com/alexcrichton/cc-rs/compare/1.0.69...1.0.70) --- updated-dependencies: - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 249eecd7..fa4ee279 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "cfg-if" From a5c9ebdf366aa548a269e8aa8fcd0b38551f5c38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 09:15:26 +0900 Subject: [PATCH 42/92] build(deps): bump signal-hook from 0.3.9 to 0.3.10 (#722) Bumps [signal-hook](https://github.com/vorner/signal-hook) from 0.3.9 to 0.3.10. - [Release notes](https://github.com/vorner/signal-hook/releases) - [Changelog](https://github.com/vorner/signal-hook/blob/master/CHANGELOG.md) - [Commits](https://github.com/vorner/signal-hook/compare/v0.3.9...v0.3.10) --- updated-dependencies: - dependency-name: signal-hook dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa4ee279..26a8700f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,9 +833,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39" +checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" dependencies = [ "libc", "signal-hook-registry", From fde0a84bbacd2b95ef5f6433f8ee8d4a91817555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 09:15:31 +0900 Subject: [PATCH 43/92] build(deps): bump tokio from 1.10.1 to 1.11.0 (#723) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.10.1 to 1.11.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.10.1...tokio-1.11.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- helix-lsp/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26a8700f..4b6ff670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -983,9 +983,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" +checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce" dependencies = [ "autocfg", "bytes", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 52e995f5..d3252af8 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.10", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } +tokio = { version = "1.11", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio-stream = "0.1.7" From 4cc562318a9987d58384fdb4e88e5a1f59f25a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 7 Sep 2021 13:00:52 +0900 Subject: [PATCH 44/92] Improve docs, fix up a few highlight scopes --- book/book.toml | 3 +- book/src/configuration.md | 13 ++ book/src/from-vim.md | 2 + book/src/install.md | 4 +- book/src/keymap.md | 100 ++++++------- book/src/themes.md | 202 ++++++++++++++++----------- book/theme/css/general.css | 13 ++ book/theme/css/variables.css | 24 ++-- book/theme/highlight.css | 101 +++++--------- runtime/queries/ocaml/highlights.scm | 2 +- runtime/queries/rust/highlights.scm | 2 +- runtime/themes/dark_plus.toml | 1 + runtime/themes/monokai.toml | 1 + theme.toml | 4 +- 14 files changed, 265 insertions(+), 207 deletions(-) diff --git a/book/book.toml b/book/book.toml index 3ccaf71e..2277a0bd 100644 --- a/book/book.toml +++ b/book/book.toml @@ -3,8 +3,9 @@ authors = ["Blaž Hrastnik"] language = "en" multilingual = false src = "src" -theme = "colibri" edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit" [output.html] cname = "docs.helix-editor.com" +default-theme = "colibri" +preferred-dark-theme = "colibri" diff --git a/book/src/configuration.md b/book/src/configuration.md index 00dfbbd8..5a28362d 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,6 +5,19 @@ To override global configuration parameters, create a `config.toml` file located * Linux and Mac: `~/.config/helix/config.toml` * Windows: `%AppData%\helix\config.toml` +## Editor + +`[editor]` section of the config. + +| Key | Description | Default | +|--|--|---------| +| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` | +| `mouse` | Enable mouse mode. | `true` | +| `middle-click-paste` | Middle click paste support. | `true` | +| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | +| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | +| `line-number` | Line number display (`absolute`, `relative`) | `absolute` | + ## LSP To display all language server messages in the status line add the following to your `config.toml`: diff --git a/book/src/from-vim.md b/book/src/from-vim.md index 8e9bbac3..09f33386 100644 --- a/book/src/from-vim.md +++ b/book/src/from-vim.md @@ -7,4 +7,6 @@ going to act on (a word, a paragraph, a line, etc) is selected first and the action itself (delete, change, yank, etc) comes second. A cursor is simply a single width selection. +See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim). + > TODO: Mention texobjects, surround, registers diff --git a/book/src/install.md b/book/src/install.md index cd9c980e..b9febbcc 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -23,7 +23,9 @@ shell for working on Helix. ### Arch Linux -Binary packages are available on AUR: +Releases are available in the `community` repository. + +Packages are also available on AUR: - [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release - [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch diff --git a/book/src/keymap.md b/book/src/keymap.md index 861e46ac..51e56eaa 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -4,7 +4,7 @@ ### Movement -> NOTE: `f`, `F`, `t` and `T` are not confined to the current line. +> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line. | Key | Description | Command | | ----- | ----------- | ------- | @@ -28,14 +28,14 @@ | `PageDown` | Move page down | `page_down` | | `Ctrl-u` | Move half page up | `half_page_up` | | `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` | +| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | | `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | | `g` | Enter [goto mode](#goto-mode) | N/A | | `m` | Enter [match mode](#match-mode) | N/A | | `:` | Enter command mode | `command_mode` | | `z` | Enter [view mode](#view-mode) | N/A | -| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) | N/A | +| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | | `Space` | Enter [space mode](#space-mode) | N/A | | `K` | Show documentation for the item under the cursor | `hover` | @@ -66,6 +66,16 @@ | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | +#### Shell + +| Key | Description | Command | +| ------ | ----------- | ------- | +| | | Pipe each selection through shell command, replacing with output | `shell_pipe` | +| A-| | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | +| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | +| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | + + ### Selection manipulation | Key | Description | Command | @@ -87,17 +97,10 @@ | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | +| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | -### Insert Mode - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `Escape` | Switch to normal mode | `normal_mode` | -| `Ctrl-x` | Autocomplete | `completion` | -| `Ctrl-w` | Delete previous word | `delete_word_backward` | - ### Search > TODO: The search implementation isn't ideal yet -- we don't support searching @@ -110,38 +113,11 @@ in reverse, or searching via smartcase. | `N` | Add next search match to selection | `extend_search_next` | | `*` | Use current selection as the search pattern | `search_selection` | -### Unimpaired - -Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired) - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `[d` | Go to previous diagnostic | `goto_prev_diag` | -| `]d` | Go to next diagnostic | `goto_next_diag` | -| `[D` | Go to first diagnostic in document | `goto_first_diag` | -| `]D` | Go to last diagnostic in document | `goto_last_diag` | -| `[space` | Add newline above | `add_newline_above` | -| `]space` | Add newline below | `add_newline_below` | - -### Shell +### Minor modes -| Key | Description | Command | -| ------ | ----------- | ------- | -| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` | -| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | -| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | -| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | -| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | - -## Select / extend mode - -I'm still pondering whether to keep this mode or not. It changes movement -commands to extend the existing selection instead of replacing it. - -> NOTE: It's a bit confusing at the moment because extend hasn't been -> implemented for all movement commands yet. +These sub-modes are accessible from normal mode and typically switch back to normal mode after a command. -## View mode +#### View mode View mode is intended for scrolling and manipulating the view without changing the selection. @@ -155,7 +131,7 @@ the selection. | `j` | Scroll the view downwards | `scroll_down` | | `k` | Scroll the view upwards | `scroll_up` | -## Goto mode +#### Goto mode Jumps to various locations. @@ -177,7 +153,7 @@ Jumps to various locations. | `i` | Go to implementation | `goto_implementation` | | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | -## Match mode +#### Match mode Enter this mode using `m` from normal mode. See the relavant section in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround) @@ -192,11 +168,9 @@ and [textobject](./usage.md#textobject) usage. | `a` `` | Select around textobject | `select_textobject_around` | | `i` `` | Select inside textobject | `select_textobject_inner` | -## Object mode - TODO: Mappings for selecting syntax nodes (a superset of `[`). -## Window mode +#### Window mode This layer is similar to vim keybindings as kakoune does not support window. @@ -207,9 +181,9 @@ This layer is similar to vim keybindings as kakoune does not support window. | `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` | | `q`, `Ctrl-q` | Close current window | `wclose` | -## Space mode +#### Space mode -This layer is a kludge of mappings I had under leader key in neovim. +This layer is a kludge of mappings, mostly pickers. | Key | Description | Command | | ----- | ----------- | ------- | @@ -226,6 +200,36 @@ This layer is a kludge of mappings I had under leader key in neovim. | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | + +#### Unimpaired + +Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `[d` | Go to previous diagnostic | `goto_prev_diag` | +| `]d` | Go to next diagnostic | `goto_next_diag` | +| `[D` | Go to first diagnostic in document | `goto_first_diag` | +| `]D` | Go to last diagnostic in document | `goto_last_diag` | +| `[space` | Add newline above | `add_newline_above` | +| `]space` | Add newline below | `add_newline_below` | + +## Insert Mode + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Escape` | Switch to normal mode | `normal_mode` | +| `Ctrl-x` | Autocomplete | `completion` | +| `Ctrl-w` | Delete previous word | `delete_word_backward` | + +## Select / extend mode + +I'm still pondering whether to keep this mode or not. It changes movement +commands (including goto) to extend the existing selection instead of replacing it. + +> NOTE: It's a bit confusing at the moment because extend hasn't been +> implemented for all movement commands yet. + # Picker Keys to use within picker. Remapping currently not supported. diff --git a/book/src/themes.md b/book/src/themes.md index 804baa1c..fe5259d5 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -30,85 +30,9 @@ if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as "key.key" = "#ffffff" ``` -Possible modifiers: +### Color palettes -| Modifier | -| --- | -| `bold` | -| `dim` | -| `italic` | -| `underlined` | -| `slow\_blink` | -| `rapid\_blink` | -| `reversed` | -| `hidden` | -| `crossed\_out` | - -Possible keys: - -| Key | Notes | -| --- | --- | -| `attribute` | | -| `keyword` | | -| `keyword.directive` | Preprocessor directives (\#if in C) | -| `keyword.control` | Control flow | -| `namespace` | | -| `punctuation` | | -| `punctuation.delimiter` | | -| `operator` | | -| `special` | | -| `property` | | -| `variable` | | -| `variable.parameter` | | -| `type` | | -| `type.builtin` | | -| `type.enum.variant` | Enum variants | -| `constructor` | | -| `function` | | -| `function.macro` | | -| `function.builtin` | | -| `comment` | | -| `variable.builtin` | | -| `constant` | | -| `constant.builtin` | | -| `string` | | -| `number` | | -| `escape` | Escaped characters | -| `label` | For lifetimes | -| `module` | | -| `ui.background` | | -| `ui.cursor` | | -| `ui.cursor.insert` | | -| `ui.cursor.select` | | -| `ui.cursor.match` | Matching bracket etc. | -| `ui.cursor.primary` | Cursor with primary selection | -| `ui.linenr` | | -| `ui.linenr.selected` | | -| `ui.statusline` | | -| `ui.statusline.inactive` | | -| `ui.popup` | | -| `ui.window` | | -| `ui.help` | | -| `ui.text` | | -| `ui.text.focus` | | -| `ui.info` | | -| `ui.info.text` | | -| `ui.menu` | | -| `ui.menu.selected` | | -| `ui.selection` | For selections in the editing area | -| `ui.selection.primary` | | -| `warning` | LSP warning | -| `error` | LSP error | -| `info` | LSP info | -| `hint` | LSP hint | - -These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences. - -For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently. - -## Color palettes - -You can define a palette of named colors, and refer to them from the +It's recommended define a palette of named colors, and refer to them from the configuration values in your theme. To do this, add a table called `palette` to your theme file: @@ -146,3 +70,125 @@ over it and is merged into the default palette. | `light-cyan` | | `light-gray` | | `white` | + +### Modifiers + +The following values may be used as modifiers. + +Less common modifiers might not be supported by your terminal emulator. + +| Modifier | +| --- | +| `bold` | +| `dim` | +| `italic` | +| `underlined` | +| `slow_blink` | +| `rapid_blink` | +| `reversed` | +| `hidden` | +| `crossed_out` | + +### Scopes + +The following is a list of scopes available to use for styling. + +#### Syntax highlighting + +These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). + +For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`. + +We use a similar set of scopes as +[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also +[TextMate](https://macromates.com/manual/en/language_grammars) scopes. + +- `escape` (TODO: rename to (constant).character.escape) + +- `type` - Types + - `builtin` - Primitive types provided by the language (`int`, `usize`) + +- `constant` (TODO: constant.other.placeholder for %v) + - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) + - `boolean` + - `character` + +- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex}) +- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)? + - `regexp` - Regular expressions + - `special` + - `path` + - `url` + +- `comment` - Code comments + - `line` - Single line comments (`//`) + - `block` - Block comments (e.g. (`/* */`) + - `documentation` - Documentation comments (e.g. `///` in Rust) + +- `variable` - Variables + - `builtin` - Reserved language variables (`self`, `this`, `super`, etc) + - `parameter` - Function parameters + - `property` + - `function` (TODO: ?) + +- `label` + +- `punctuation` + - `delimiter` - Commas, colons + - `bracket` - Parentheses, angle brackets, etc. + +- `keyword` + - `control` + - `conditional` - `if`, `else` + - `repeat` - `for`, `while`, `loop` + - `import` - `import`, `export` + - (TODO: return?) + - `directive` - Preprocessor directives (`#if` in C) + - `function` - `fn`, `func` + +- `operator` - `||`, `+=`, `>`, `or` + +- `function` + - `builtin` + - `method` + - `macro` + - `special` (preprocesor in C) + +- `tag` - Tags (e.g. `` in HTML) + +- `namespace` + +#### Interface + +These scopes are used for theming the editor interface. + + +| Key | Notes | +| --- | --- | +| `ui.background` | | +| `ui.cursor` | | +| `ui.cursor.insert` | | +| `ui.cursor.select` | | +| `ui.cursor.match` | Matching bracket etc. | +| `ui.cursor.primary` | Cursor with primary selection | +| `ui.linenr` | | +| `ui.linenr.selected` | | +| `ui.statusline` | Statusline | +| `ui.statusline.inactive` | Statusline (unfocused document) | +| `ui.popup` | | +| `ui.window` | | +| `ui.help` | | +| `ui.text` | | +| `ui.text.focus` | | +| `ui.info` | | +| `ui.info.text` | | +| `ui.menu` | | +| `ui.menu.selected` | | +| `ui.selection` | For selections in the editing area | +| `ui.selection.primary` | | +| `warning` | Diagnostics warning | +| `error` | Diagnostics error | +| `info` | Diagnostics info | +| `hint` | Diagnostics hint | + + diff --git a/book/theme/css/general.css b/book/theme/css/general.css index 7749bded..ddc2387a 100644 --- a/book/theme/css/general.css +++ b/book/theme/css/general.css @@ -114,6 +114,19 @@ h6:target::before { margin-bottom: .875em; } +.content ul li { +margin-bottom: .25rem; +} +.content ul { + list-style-type: square; +} +.content ul ul, .content ol ul { + margin-bottom: .5rem; +} +.content li p { + margin-bottom: .5em; +} + .content p { line-height: 1.45em; } .content ol { line-height: 1.45em; } .content ul { line-height: 1.45em; } diff --git a/book/theme/css/variables.css b/book/theme/css/variables.css index a49d6794..db1a11b8 100644 --- a/book/theme/css/variables.css +++ b/book/theme/css/variables.css @@ -69,7 +69,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; --theme-popup-bg: #141617; --theme-popup-border: #43484d; @@ -110,7 +110,7 @@ --links: #20609f; - --inline-code-color: #301900; + --inline-code-color: #a39e9b; --theme-popup-bg: #fafafa; --theme-popup-border: #cccccc; @@ -151,7 +151,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -192,7 +192,7 @@ --links: #2b79a2; - --inline-code-color: #6e6b5e; + --inline-code-color: #c5c8c6; --theme-popup-bg: #e1e1db; --theme-popup-border: #b38f6b; @@ -234,7 +234,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #6e6b5e; --theme-popup-bg: #141617; --theme-popup-border: #43484d; @@ -261,6 +261,7 @@ .colibri { --bg: #3b224c; --fg: #bcbdd0; + --heading-fg: #fff; --sidebar-bg: #281733; --sidebar-fg: #c8c9db; @@ -276,18 +277,19 @@ /* --links: #a4a0e8; */ --links: #ECCDBA; - --inline-code-color: #c5c8c6;; + --inline-code-color: hsl(48.7, 7.8%, 70%); --theme-popup-bg: #161923; --theme-popup-border: #737480; --theme-hover: rgba(0,0,0, .2); - --quote-bg: hsl(226, 15%, 17%); + --quote-bg: #281733; --quote-border: hsl(226, 15%, 22%); - --table-border-color: hsl(226, 23%, 16%); - --table-header-bg: hsl(226, 23%, 31%); + --table-border-color: hsl(226, 23%, 76%); + --table-header-bg: hsla(226, 23%, 31%, 0); --table-alternate-bg: hsl(226, 23%, 14%); + --table-border-line: hsla(201deg, 20%, 92%, 0.2); --searchbar-border-color: #aaa; --searchbar-bg: #aeaec6; @@ -300,6 +302,7 @@ } .colibri { +/* --bg: #ffffff; --fg: #452859; --fg: #5a5977; @@ -318,7 +321,7 @@ --links: #6F44F0; - --inline-code-color: #697C81; + --inline-code-color: #a39e9b; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -341,4 +344,5 @@ --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; --search-mark-bg: #a2cff5; +*/ } diff --git a/book/theme/highlight.css b/book/theme/highlight.css index c2343227..8dce7d65 100644 --- a/book/theme/highlight.css +++ b/book/theme/highlight.css @@ -1,83 +1,56 @@ -/* - * An increased contrast highlighting scheme loosely based on the - * "Base16 Atelier Dune Light" theme by Bram de Haan - * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) - * Original Base16 color scheme by Chris Kempson - * (https://github.com/chriskempson/base16) - */ - -/* Comment */ +pre code.hljs { + display:block; + overflow-x:auto; + padding:1em +} +code.hljs { + padding:3px 5px +} +.hljs { + background:#2f1e2e; + color:#a39e9b +} .hljs-comment, .hljs-quote { - color: #575757; + color:#8d8687 } - -/* Red */ -.hljs-variable, -.hljs-template-variable, -.hljs-attribute, -.hljs-tag, -.hljs-name, -.hljs-regexp, .hljs-link, +.hljs-meta, .hljs-name, +.hljs-regexp, +.hljs-selector-class, .hljs-selector-id, -.hljs-selector-class { - color: #d70025; +.hljs-tag, +.hljs-template-variable, +.hljs-variable { + color:#ef6155 } - -/* Orange */ -.hljs-number, -.hljs-meta, .hljs-built_in, -.hljs-builtin-name, +.hljs-deletion, .hljs-literal, -.hljs-type, -.hljs-params { - color: #b21e00; +.hljs-number, +.hljs-params, +.hljs-type { + color:#f99b15 } - -/* Green */ -.hljs-string, -.hljs-symbol, -.hljs-bullet { - color: #008200; +.hljs-attribute, +.hljs-section, +.hljs-title { + color:#fec418 } - -/* Blue */ -.hljs-title, -.hljs-section { - color: #0030f2; +.hljs-addition, +.hljs-bullet, +.hljs-string, +.hljs-symbol { + color:#48b685 } - -/* Purple */ .hljs-keyword, .hljs-selector-tag { - color: #9d00ec; -} - -.hljs { - display: block; - overflow-x: auto; - background: #f6f7f6; - color: #000; - padding: 0.5em; + color:#815ba4 } - .hljs-emphasis { - font-style: italic; + font-style:italic } - .hljs-strong { - font-weight: bold; -} - -.hljs-addition { - color: #22863a; - background-color: #f0fff4; -} - -.hljs-deletion { - color: #b31d28; - background-color: #ffeef0; + font-weight:700 } diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 5554574d..160f2cb4 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -60,7 +60,7 @@ [(number) (signed_number)] @number -(character) @constant.builtin.character +(character) @constant.character (string) @string diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index c76d6adf..956a5dac 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -149,7 +149,7 @@ (mutable_specifier) @keyword.mut - +; TODO: variable.mut to highlight mutable identifiers via locals.scm ; ------- ; Guess Other Types diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index c105d52b..7eeb3f95 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#6A9955" } "string" = { fg = "#ce9178" } +"string.regexp" = { fg = "regex" } "number" = { fg = "#b5cea8" } "escape" = { fg = "#d7ba7d" } diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index 2407591a..a8f03ff3 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#88846F" } "string" = { fg = "#e6db74" } +"string.regexp" = { fg = "regex" } "number" = { fg = "#ae81ff" } "escape" = { fg = "#ae81ff" } diff --git a/theme.toml b/theme.toml index 867a0d2c..49f46b0b 100644 --- a/theme.toml +++ b/theme.toml @@ -28,9 +28,7 @@ escape = "honey" label = "honey" # TODO: diferentiate doc comment -# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors - -module = "#ff0000" +# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors "ui.background" = { bg = "midnight" } "ui.linenr" = { fg = "comet" } From 3cbdc057de69b3ffaf1e8b69dea114872d9d128a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 7 Sep 2021 13:05:20 +0900 Subject: [PATCH 45/92] lsp: Don't import SymbolServer for Julia anymore, it's not necessary --- languages.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/languages.toml b/languages.toml index c1ddbea2..a29dd38d 100644 --- a/languages.toml +++ b/languages.toml @@ -224,7 +224,6 @@ language-server = { command = "julia", args = [ using LanguageServer; using Pkg; import StaticLint; - import SymbolServer; env_path = dirname(Pkg.Types.Context().env.project_file); server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); From 7a9db951829d37447a414f03802297f4b43e02a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kangwook=20Lee=20=28=EC=9D=B4=EA=B0=95=EC=9A=B1=29?= Date: Tue, 7 Sep 2021 23:22:39 +0900 Subject: [PATCH 46/92] Add command to extend to line start or end (#717) --- helix-term/src/commands.rs | 39 ++++++++++++++++++++++++++++++++------ helix-term/src/keymap.rs | 4 ++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2fbed6b3..38e65537 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -252,6 +252,9 @@ impl Command { // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", + extend_to_line_start, "Extend to line start", + extend_to_line_end, "Extend to line end", + extend_to_line_end_newline, "Extend to line end", signature_help, "Show signature help", insert_tab, "Insert tab char", insert_newline, "Insert newline char", @@ -407,7 +410,7 @@ fn extend_line_down(cx: &mut Context) { move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) } -fn goto_line_end(cx: &mut Context) { +fn goto_line_end_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -418,12 +421,20 @@ fn goto_line_end(cx: &mut Context) { let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line)) .max(line_start); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } -fn goto_line_end_newline(cx: &mut Context) { +fn goto_line_end(cx: &mut Context) { + goto_line_end_impl(cx, Movement::Move) +} + +fn extend_to_line_end(cx: &mut Context) { + goto_line_end_impl(cx, Movement::Extend) +} + +fn goto_line_end_newline_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -431,12 +442,20 @@ fn goto_line_end_newline(cx: &mut Context) { let line = range.cursor_line(text); let pos = line_end_char_index(&text, line); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } -fn goto_line_start(cx: &mut Context) { +fn goto_line_end_newline(cx: &mut Context) { + goto_line_end_newline_impl(cx, Movement::Move) +} + +fn extend_to_line_end_newline(cx: &mut Context) { + goto_line_end_newline_impl(cx, Movement::Extend) +} + +fn goto_line_start_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -445,11 +464,19 @@ fn goto_line_start(cx: &mut Context) { // adjust to start of the line let pos = text.line_to_char(line); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } +fn goto_line_start(cx: &mut Context) { + goto_line_start_impl(cx, Movement::Move) +} + +fn extend_to_line_start(cx: &mut Context) { + goto_line_start_impl(cx, Movement::Extend) +} + fn goto_first_nonwhitespace(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index aa60482d..1b9d87b5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -592,8 +592,8 @@ impl Default for Keymaps { "T" => extend_till_prev_char, "F" => extend_prev_char, - "home" => goto_line_start, - "end" => goto_line_end, + "home" => extend_to_line_start, + "end" => extend_to_line_end, "esc" => exit_select_mode, "v" => normal_mode, From 89f0dbe8e8108a4b1b55440345e4990afd4b8489 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Tue, 7 Sep 2021 22:22:53 +0800 Subject: [PATCH 47/92] Update tree-sitter-ledger (#724) --- helix-syntax/languages/tree-sitter-ledger | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-syntax/languages/tree-sitter-ledger b/helix-syntax/languages/tree-sitter-ledger index 72319504..0cdeb0e5 160000 --- a/helix-syntax/languages/tree-sitter-ledger +++ b/helix-syntax/languages/tree-sitter-ledger @@ -1 +1 @@ -Subproject commit 72319504776f14193472a6ad14abec0af0225cbe +Subproject commit 0cdeb0e51411a3ba5493662952c3039de08939ca From f871d318c00023416dbc21747fd227f4a24c94c6 Mon Sep 17 00:00:00 2001 From: Raphael Megzari Date: Tue, 7 Sep 2021 23:23:05 +0900 Subject: [PATCH 48/92] add language server for elixir and nix (#725) --- languages.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/languages.toml b/languages.toml index a29dd38d..5c147770 100644 --- a/languages.toml +++ b/languages.toml @@ -48,6 +48,7 @@ file-types = ["ex", "exs"] roots = [] comment-token = "#" +language-server = { command = "elixir-ls" } indent = { tab-width = 2, unit = " " } [[language]] @@ -165,6 +166,7 @@ file-types = ["nix"] roots = [] comment-token = "#" +language-server = { command = "rnix-lsp" } indent = { tab-width = 2, unit = " " } [[language]] From 2ce87968cd5983167271cd306ab1c1d72a9c488d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 8 Sep 2021 14:19:25 +0900 Subject: [PATCH 49/92] ui: Be smarter about centering previews Try centering the whole block. If the block is too big for the viewport, then make sure that the first line is within the preview. --- helix-term/src/ui/picker.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 06e424ea..e040e0ff 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -124,9 +124,13 @@ impl Component for FilePicker { }) { // align to middle let first_line = line - .map(|(start, _)| start) - .unwrap_or(0) - .saturating_sub(inner.height as usize / 2); + .map(|(start, end)| { + let height = end.saturating_sub(start) + 1; + let middle = start + (height.saturating_sub(1) / 2); + middle.saturating_sub(inner.height as usize / 2).min(start) + }) + .unwrap_or(0); + let offset = Position::new(first_line, 0); let highlights = EditorView::doc_syntax_highlights( From 011f9aa47f2316f120da48d342430c7c5caaf107 Mon Sep 17 00:00:00 2001 From: CossonLeo <20379044+cossonleo@users.noreply.github.com> Date: Wed, 8 Sep 2021 07:33:59 +0000 Subject: [PATCH 50/92] Optimize completion doc position. (#691) * optimize completion doc's render * optimize completion doc's render * optimize completion doc position * cargo fmt * fix panic * use saturating_sub * fixs * fix clippy * limit completion doc max width 120 --- helix-term/src/ui/completion.rs | 45 ++++++++++++++++++++--------- helix-term/src/ui/markdown.rs | 28 +++++++++++++++--- helix-term/src/ui/popup.rs | 50 +++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 90657764..6c9e3a80 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -262,8 +262,7 @@ impl Component for Completion { .cursor(doc.text().slice(..)); let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row - view.offset.row) as u16; - - let mut doc = match &option.documentation { + let mut markdown_doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::PlainText, @@ -311,24 +310,42 @@ impl Component for Completion { None => return, }; - let half = area.height / 2; - let height = 15.min(half); - // we want to make sure the cursor is visible (not hidden behind the documentation) - let y = if cursor_pos + area.y - >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) - { - 0 + let (popup_x, popup_y) = self.popup.get_rel_position(area, cx); + let (popup_width, _popup_height) = self.popup.get_size(); + let mut width = area + .width + .saturating_sub(popup_x) + .saturating_sub(popup_width); + let area = if width > 30 { + let mut height = area.height.saturating_sub(popup_y); + let x = popup_x + popup_width; + let y = popup_y; + + if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { + width = rel_width; + height = rel_height; + } + Rect::new(x, y, width, height) } else { - // -2 to subtract command line + statusline. a bit of a hack, because of splits. - area.height.saturating_sub(height).saturating_sub(2) - }; + let half = area.height / 2; + let height = 15.min(half); + // we want to make sure the cursor is visible (not hidden behind the documentation) + let y = if cursor_pos + area.y + >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) + { + 0 + } else { + // -2 to subtract command line + statusline. a bit of a hack, because of splits. + area.height.saturating_sub(height).saturating_sub(2) + }; - let area = Rect::new(0, y, area.width, height); + Rect::new(0, y, area.width, height) + }; // clear area let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - doc.render(area, surface, cx); + markdown_doc.render(area, surface, cx); } } } diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 28542cdc..87b35a2d 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -215,10 +215,30 @@ impl Component for Markdown { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = parse(&self.contents, None, &self.config_loader); let padding = 2; - let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); - let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); - Some((width, height)) + if padding >= viewport.1 || padding >= viewport.0 { + return None; + } + let contents = parse(&self.contents, None, &self.config_loader); + let max_text_width = (viewport.0 - padding).min(120); + let mut text_width = 0; + let mut height = padding; + for content in contents { + height += 1; + let content_width = content.width() as u16; + if content_width > max_text_width { + text_width = max_text_width; + height += content_width / max_text_width; + } else if content_width > text_width { + text_width = content_width; + } + + if height >= viewport.1 { + height = viewport.1; + break; + } + } + + Some((text_width + padding, height)) } } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index e126c845..846cefb8 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -31,6 +31,33 @@ impl Popup { self.position = pos; } + pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { + let position = self + .position + .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); + + let (width, height) = self.size; + + // -- make sure frame doesn't stick out of bounds + let mut rel_x = position.col as u16; + let rel_y = position.row as u16; + if viewport.width <= rel_x + width { + rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); + }; + + // TODO: be able to specify orientation preference. We want above for most popups, below + // for menus/autocomplete. + if height <= rel_y { + (rel_x, rel_y.saturating_sub(height)) // position above point + } else { + (rel_x, rel_y + 1) // position below point + } + } + + pub fn get_size(&self) -> (u16, u16) { + (self.size.0, self.size.1) + } + pub fn scroll(&mut self, offset: usize, direction: bool) { if direction { self.scroll += offset; @@ -108,29 +135,10 @@ impl Component for Popup { fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { cx.scroll = Some(self.scroll); - let position = self - .position - .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); - - let (width, height) = self.size; - - // -- make sure frame doesn't stick out of bounds - let mut rel_x = position.col as u16; - let mut rel_y = position.row as u16; - if viewport.width <= rel_x + width { - rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - }; - - // TODO: be able to specify orientation preference. We want above for most popups, below - // for menus/autocomplete. - if height <= rel_y { - rel_y = rel_y.saturating_sub(height) // position above point - } else { - rel_y += 1 // position below point - } + let (rel_x, rel_y) = self.get_rel_position(viewport, cx); // clip to viewport - let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height)); + let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)); // clear area let background = cx.editor.theme.get("ui.popup"); From 72cf86e4626c01c6ce2371c7b134ab7a7041620c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 8 Sep 2021 14:52:09 +0900 Subject: [PATCH 51/92] Regex prompts should have a history with a specifiable register --- helix-term/src/commands.rs | 38 +++++++++++----------- helix-term/src/ui/editor.rs | 4 +-- helix-term/src/ui/mod.rs | 9 +++--- helix-view/src/editor.rs | 6 ++-- helix-view/src/lib.rs | 2 -- helix-view/src/register_selection.rs | 48 ---------------------------- 6 files changed, 29 insertions(+), 78 deletions(-) delete mode 100644 helix-view/src/register_selection.rs diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 38e65537..841af22a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -44,7 +44,7 @@ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; pub struct Context<'a> { - pub selected_register: helix_view::RegisterSelection, + pub register: Option, pub count: Option, pub editor: &'a mut Editor, @@ -1030,7 +1030,8 @@ fn select_all(cx: &mut Context) { } fn select_regex(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| { + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt(cx, "select:".into(), Some(reg), move |view, doc, regex| { let text = doc.text().slice(..); if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex) { @@ -1042,7 +1043,8 @@ fn select_regex(cx: &mut Context) { } fn split_selection(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| { + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt(cx, "split:".into(), Some(reg), move |view, doc, regex| { let text = doc.text().slice(..); let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); doc.set_selection(view.id, selection); @@ -1100,6 +1102,7 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege // TODO: use one function for search vs extend fn search(cx: &mut Context) { + let reg = cx.register.unwrap_or('/'); let (_, doc) = current!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1108,10 +1111,8 @@ fn search(cx: &mut Context) { // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); - let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| { + let prompt = ui::regex_prompt(cx, "search:".into(), Some(reg), move |view, doc, regex| { search_impl(doc, view, &contents, ®ex, false); - // TODO: only store on enter (accept), not update - registers.write('/', vec![regex.as_str().to_string()]); }); cx.push_layer(Box::new(prompt)); @@ -1119,9 +1120,9 @@ fn search(cx: &mut Context) { fn search_next_impl(cx: &mut Context, extend: bool) { let (view, doc) = current!(cx.editor); - let registers = &mut cx.editor.registers; + let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { - let query = query.first().unwrap(); + let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); let regex = Regex::new(query).unwrap(); search_impl(doc, view, &contents, ®ex, extend); @@ -1141,7 +1142,7 @@ fn search_selection(cx: &mut Context) { let contents = doc.text().slice(..); let query = doc.selection(view.id).primary().fragment(contents); let regex = regex::escape(&query); - cx.editor.registers.write('/', vec![regex]); + cx.editor.registers.get_mut('/').push(regex); search_next(cx); } @@ -1200,7 +1201,7 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId } fn delete_selection(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; let reg = registers.get_mut(reg_name); @@ -1213,7 +1214,7 @@ fn delete_selection(cx: &mut Context) { } fn change_selection(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; let reg = registers.get_mut(reg_name); @@ -3346,12 +3347,12 @@ fn yank(cx: &mut Context) { let msg = format!( "yanked {} selection(s) to register {}", values.len(), - cx.selected_register.name() + cx.register.unwrap_or('"') ); cx.editor .registers - .write(cx.selected_register.name(), values); + .write(cx.register.unwrap_or('"'), values); cx.editor.set_status(msg); exit_select_mode(cx); @@ -3524,7 +3525,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) { } fn replace_with_yanked(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3575,7 +3576,7 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) { } fn paste_after(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3589,7 +3590,7 @@ fn paste_after(cx: &mut Context) { } fn paste_before(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3770,7 +3771,8 @@ fn join_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) { // keep selections matching regex - let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| { + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt(cx, "keep:".into(), Some(reg), move |view, doc, regex| { let text = doc.text().slice(..); if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { @@ -4103,7 +4105,7 @@ fn wclose(cx: &mut Context) { fn select_register(cx: &mut Context) { cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - cx.editor.selected_register.select(ch); + cx.editor.selected_register = Some(ch); } }) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index e8cd40cf..52cf3d2b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -710,7 +710,7 @@ impl EditorView { // debug_assert!(cxt.count != 0); // set the register - cxt.selected_register = cxt.editor.selected_register.take(); + cxt.register = cxt.editor.selected_register.take(); self.handle_keymap_event(mode, cxt, event); if self.keymaps.pending().is_empty() { @@ -887,9 +887,9 @@ impl EditorView { impl Component for EditorView { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let mut cxt = commands::Context { - selected_register: helix_view::RegisterSelection::default(), editor: &mut cx.editor, count: None, + register: None, callback: None, on_next_key_callback: None, jobs: cx.jobs, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0a1e24b5..07eef352 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,7 +20,6 @@ pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; use helix_core::regex::Regex; -use helix_core::register::Registers; use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -28,7 +27,8 @@ use std::path::PathBuf; pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, - fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static, + history_register: Option, + fun: impl Fn(&mut View, &mut Document, Regex) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); let view_id = view.id; @@ -36,7 +36,7 @@ pub fn regex_prompt( Prompt::new( prompt, - None, + history_register, |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { @@ -56,12 +56,11 @@ pub fn regex_prompt( match Regex::new(input) { Ok(regex) => { let (view, doc) = current!(cx.editor); - let registers = &mut cx.editor.registers; // revert state to what it was before the last update doc.set_selection(view.id, snapshot.clone()); - fun(view, doc, registers, regex); + fun(view, doc, regex); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3d2d4a87..52a0060c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -3,7 +3,7 @@ use crate::{ graphics::{CursorKind, Rect}, theme::{self, Theme}, tree::Tree, - Document, DocumentId, RegisterSelection, View, ViewId, + Document, DocumentId, View, ViewId, }; use futures_util::future; @@ -73,7 +73,7 @@ pub struct Editor { pub tree: Tree, pub documents: SlotMap, pub count: Option, - pub selected_register: RegisterSelection, + pub selected_register: Option, pub registers: Registers, pub theme: Theme, pub language_servers: helix_lsp::Registry, @@ -111,7 +111,7 @@ impl Editor { tree: Tree::new(area), documents: SlotMap::with_key(), count: None, - selected_register: RegisterSelection::default(), + selected_register: None, theme: themes.default(), language_servers, syn_loader: config_loader, diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 9bcc0b7d..c37474d6 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -8,7 +8,6 @@ pub mod graphics; pub mod info; pub mod input; pub mod keyboard; -pub mod register_selection; pub mod theme; pub mod tree; pub mod view; @@ -20,6 +19,5 @@ slotmap::new_key_type! { pub use document::Document; pub use editor::Editor; -pub use register_selection::RegisterSelection; pub use theme::Theme; pub use view::View; diff --git a/helix-view/src/register_selection.rs b/helix-view/src/register_selection.rs deleted file mode 100644 index a2b78f72..00000000 --- a/helix-view/src/register_selection.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// Register selection and configuration -/// -/// This is a kind a of specialized `Option` for register selection. -/// Point is to keep whether the register selection has been explicitely -/// set or not while being convenient by knowing the default register name. -#[derive(Debug)] -pub struct RegisterSelection { - selected: char, - default_name: char, -} - -impl RegisterSelection { - pub fn new(default_name: char) -> Self { - Self { - selected: default_name, - default_name, - } - } - - pub fn select(&mut self, name: char) { - self.selected = name; - } - - pub fn take(&mut self) -> Self { - Self { - selected: std::mem::replace(&mut self.selected, self.default_name), - default_name: self.default_name, - } - } - - pub fn is_default(&self) -> bool { - self.selected == self.default_name - } - - pub fn name(&self) -> char { - self.selected - } -} - -impl Default for RegisterSelection { - fn default() -> Self { - let default_name = '"'; - Self { - selected: default_name, - default_name, - } - } -} From d991715ff1fbd906fab60dda1f70f700c9e14817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 8 Sep 2021 14:52:30 +0900 Subject: [PATCH 52/92] Switch rust-toolchain.toml over to stable --- rust-toolchain.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a63c14b9..79f6f8f6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly" -# components = ["rustfmt", "rust-src"] +channel = "stable" +components = ["rustfmt", "rust-src"] From 3426285a6341702ace35817d38cefd2bb8b16437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 8 Sep 2021 14:58:11 +0900 Subject: [PATCH 53/92] fix: Don't automatically search_next on * Refs #713 --- helix-term/src/commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 841af22a..a7a71576 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1143,7 +1143,8 @@ fn search_selection(cx: &mut Context) { let query = doc.selection(view.id).primary().fragment(contents); let regex = regex::escape(&query); cx.editor.registers.get_mut('/').push(regex); - search_next(cx); + let msg = format!("register '{}' set to '{}'", '\\', query); + cx.editor.set_status(msg); } fn extend_line(cx: &mut Context) { From 394cc4f30f23d6ffbea70e714167ce04e37e02ce Mon Sep 17 00:00:00 2001 From: cbarrete <62146989+cbarrete@users.noreply.github.com> Date: Wed, 8 Sep 2021 21:13:11 -0400 Subject: [PATCH 54/92] Update ledger treesitter injections (#732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cédric Barreteau --- runtime/queries/ledger/injections.scm | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/queries/ledger/injections.scm b/runtime/queries/ledger/injections.scm index 4bb7d675..2d948141 100644 --- a/runtime/queries/ledger/injections.scm +++ b/runtime/queries/ledger/injections.scm @@ -1 +1,2 @@ (comment) @comment +(note) @comment From bb47a9a0b8944a5933d45b2c7b6c22cb2e96c4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 9 Sep 2021 11:48:08 +0900 Subject: [PATCH 55/92] fix: Fix regression where formatting would fail on null response --- helix-lsp/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f2bb0059..4068ae1f 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -614,8 +614,8 @@ impl Client { Some(async move { let json = request.await?; - let response: Vec = serde_json::from_value(json)?; - Ok(response) + let response: Option> = serde_json::from_value(json)?; + Ok(response.unwrap_or_default()) }) } From 0b1bc566e4e3cfa88e23133fb018c998f2052c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 9 Sep 2021 11:54:01 +0900 Subject: [PATCH 56/92] fix: lsp: Regression with textDocument/didSave not getting sent --- helix-view/src/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 6de60995..1f1b1f5f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -472,7 +472,7 @@ impl Document { to_writer(&mut file, encoding, &text).await?; if let Some(language_server) = language_server { - if language_server.is_initialized() { + if !language_server.is_initialized() { return Ok(()); } if let Some(notification) = From 94abc52b3b0929f399cea14e1efcf2c1d0a31ad8 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 10 Sep 2021 19:44:23 +0530 Subject: [PATCH 57/92] feat: Sticky view mode with Z (#719) --- book/src/keymap.md | 74 ++++++++++++++++++++++------------------ helix-term/src/keymap.rs | 16 +++++++++ 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 51e56eaa..4fa5033d 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -6,38 +6,39 @@ > NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line. -| Key | Description | Command | -| ----- | ----------- | ------- | -| `h`, `Left` | Move left | `move_char_left` | -| `j`, `Down` | Move down | `move_char_right` | -| `k`, `Up` | Move up | `move_line_up` | -| `l`, `Right` | Move right | `move_line_down` | -| `w` | Move next word start | `move_next_word_start` | -| `b` | Move previous word start | `move_prev_word_start` | -| `e` | Move next word end | `move_next_word_end` | -| `W` | Move next WORD start | `move_next_long_word_start` | -| `B` | Move previous WORD start | `move_prev_long_word_start` | -| `E` | Move next WORD end | `move_next_long_word_end` | -| `t` | Find 'till next char | `find_till_char` | -| `f` | Find next char | `find_next_char` | -| `T` | Find 'till previous char | `till_prev_char` | -| `F` | Find previous char | `find_prev_char` | -| `Home` | Move to the start of the line | `goto_line_start` | -| `End` | Move to the end of the line | `goto_line_end` | -| `PageUp` | Move page up | `page_up` | -| `PageDown` | Move page down | `page_down` | -| `Ctrl-u` | Move half page up | `half_page_up` | -| `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | -| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | -| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | -| `g` | Enter [goto mode](#goto-mode) | N/A | -| `m` | Enter [match mode](#match-mode) | N/A | -| `:` | Enter command mode | `command_mode` | -| `z` | Enter [view mode](#view-mode) | N/A | -| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | -| `Space` | Enter [space mode](#space-mode) | N/A | -| `K` | Show documentation for the item under the cursor | `hover` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `h`, `Left` | Move left | `move_char_left` | +| `j`, `Down` | Move down | `move_char_right` | +| `k`, `Up` | Move up | `move_line_up` | +| `l`, `Right` | Move right | `move_line_down` | +| `w` | Move next word start | `move_next_word_start` | +| `b` | Move previous word start | `move_prev_word_start` | +| `e` | Move next word end | `move_next_word_end` | +| `W` | Move next WORD start | `move_next_long_word_start` | +| `B` | Move previous WORD start | `move_prev_long_word_start` | +| `E` | Move next WORD end | `move_next_long_word_end` | +| `t` | Find 'till next char | `find_till_char` | +| `f` | Find next char | `find_next_char` | +| `T` | Find 'till previous char | `till_prev_char` | +| `F` | Find previous char | `find_prev_char` | +| `Home` | Move to the start of the line | `goto_line_start` | +| `End` | Move to the end of the line | `goto_line_end` | +| `PageUp` | Move page up | `page_up` | +| `PageDown` | Move page down | `page_down` | +| `Ctrl-u` | Move half page up | `half_page_up` | +| `Ctrl-d` | Move half page down | `half_page_down` | +| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | +| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | +| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | +| `g` | Enter [goto mode](#goto-mode) | N/A | +| `m` | Enter [match mode](#match-mode) | N/A | +| `:` | Enter command mode | `command_mode` | +| `z` | Enter [view mode](#view-mode) | N/A | +| `Z` | Enter sticky [view mode](#view-mode) | N/A | +| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | +| `Space` | Enter [space mode](#space-mode) | N/A | +| `K` | Show documentation for the item under the cursor | `hover` | ### Changes @@ -120,7 +121,10 @@ These sub-modes are accessible from normal mode and typically switch back to nor #### View mode View mode is intended for scrolling and manipulating the view without changing -the selection. +the selection. The "sticky" variant of this mode is persistent; use the Escape +key to return to normal mode after usage (useful when you're simply looking +over text and not actively editing it). + | Key | Description | Command | | ----- | ----------- | ------- | @@ -130,6 +134,10 @@ the selection. | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `j` | Scroll the view downwards | `scroll_down` | | `k` | Scroll the view upwards | `scroll_up` | +| `f` | Move page down | `page_down` | +| `b` | Move page up | `page_up` | +| `d` | Move half page down | `half_page_down` | +| `u` | Move half page up | `half_page_up` | #### Goto mode diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 1b9d87b5..f38c8a40 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -563,6 +563,22 @@ impl Default for Keymaps { "m" => align_view_middle, "k" => scroll_up, "j" => scroll_down, + "b" => page_up, + "f" => page_down, + "u" => half_page_up, + "d" => half_page_down, + }, + "Z" => { "View" sticky=true + "z" | "c" => align_view_center, + "t" => align_view_top, + "b" => align_view_bottom, + "m" => align_view_middle, + "k" => scroll_up, + "j" => scroll_down, + "b" => page_up, + "f" => page_down, + "u" => half_page_up, + "d" => half_page_down, }, "\"" => select_register, From 987d8e6dd66d65c2503cc81a3b9ea8787435839a Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Fri, 10 Sep 2021 11:12:26 -0400 Subject: [PATCH 58/92] Convert clipboard line ending to document line ending when pasting (#716) * convert a paste's line-ending to the current doc's line-ending * move paste regex into paste_impl --- helix-term/src/commands.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a7a71576..f9ebb801 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3462,7 +3462,14 @@ fn paste_impl( .iter() .any(|value| get_line_ending_of_str(value).is_some()); - let mut values = values.iter().cloned().map(Tendril::from).chain(repeat); + // Only compiled once. + #[allow(clippy::trivial_regex)] + static REGEX: Lazy = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); + let mut values = values + .iter() + .map(|value| REGEX.replace_all(value, doc.line_ending.as_str())) + .map(|value| Tendril::from(value.as_ref())) + .chain(repeat); let text = doc.text(); let selection = doc.selection(view.id); From 05c2a72ccb7f79e8e581d2703816c74543d1995c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kangwook=20Lee=20=28=EC=9D=B4=EA=B0=95=EC=9A=B1=29?= Date: Sat, 11 Sep 2021 18:31:40 +0900 Subject: [PATCH 59/92] goto line start/end commands extend when in select mode (#739) --- helix-term/src/commands.rs | 51 +++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f9ebb801..fb885740 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -410,8 +410,7 @@ fn extend_line_down(cx: &mut Context) { move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) } -fn goto_line_end_impl(cx: &mut Context, movement: Movement) { - let (view, doc) = current!(cx.editor); +fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -427,15 +426,24 @@ fn goto_line_end_impl(cx: &mut Context, movement: Movement) { } fn goto_line_end(cx: &mut Context) { - goto_line_end_impl(cx, Movement::Move) + let (view, doc) = current!(cx.editor); + goto_line_end_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) } fn extend_to_line_end(cx: &mut Context) { - goto_line_end_impl(cx, Movement::Extend) + let (view, doc) = current!(cx.editor); + goto_line_end_impl(view, doc, Movement::Extend) } -fn goto_line_end_newline_impl(cx: &mut Context, movement: Movement) { - let (view, doc) = current!(cx.editor); +fn goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -448,15 +456,24 @@ fn goto_line_end_newline_impl(cx: &mut Context, movement: Movement) { } fn goto_line_end_newline(cx: &mut Context) { - goto_line_end_newline_impl(cx, Movement::Move) + let (view, doc) = current!(cx.editor); + goto_line_end_newline_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) } fn extend_to_line_end_newline(cx: &mut Context) { - goto_line_end_newline_impl(cx, Movement::Extend) + let (view, doc) = current!(cx.editor); + goto_line_end_newline_impl(view, doc, Movement::Extend) } -fn goto_line_start_impl(cx: &mut Context, movement: Movement) { - let (view, doc) = current!(cx.editor); +fn goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -470,11 +487,21 @@ fn goto_line_start_impl(cx: &mut Context, movement: Movement) { } fn goto_line_start(cx: &mut Context) { - goto_line_start_impl(cx, Movement::Move) + let (view, doc) = current!(cx.editor); + goto_line_start_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) } fn extend_to_line_start(cx: &mut Context) { - goto_line_start_impl(cx, Movement::Extend) + let (view, doc) = current!(cx.editor); + goto_line_start_impl(view, doc, Movement::Extend) } fn goto_first_nonwhitespace(cx: &mut Context) { From 004c8fd4628d639fca999672a43a8ab81c3cdea3 Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Fri, 10 Sep 2021 19:48:04 +0300 Subject: [PATCH 60/92] chore(nix): update flake inputs and submodule --- flake.lock | 18 +++++++++--------- flake.nix | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flake.lock b/flake.lock index d5271a92..21e44c6e 100644 --- a/flake.lock +++ b/flake.lock @@ -40,11 +40,11 @@ "rustOverlay": "rustOverlay" }, "locked": { - "lastModified": 1630822133, - "narHash": "sha256-CoPUuZvXx690gj5O1EljXNyGInBDfqWC4MOXARMrHog=", + "lastModified": 1631254163, + "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "e9e445cd6694171a3393df8fd016356baec5742f", + "rev": "432d8504a32232e8d74710024d5bf5cc31767651", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1630850248, - "narHash": "sha256-OzJi6Olf7mSVhGt3W7qOMVP5Qk1lH60zlHeCcITzfv0=", + "lastModified": 1631206977, + "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "23d5823337f4502dfa17e192d8c53a47cabcb6b4", + "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", "type": "github" }, "original": { @@ -79,11 +79,11 @@ "rustOverlay": { "flake": false, "locked": { - "lastModified": 1630808055, - "narHash": "sha256-axfnFXuakMyLMlNIr1yVzvC8ipj6nVJnia2YtkWdzYg=", + "lastModified": 1631240108, + "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "b4c01ab310031a557b6a00062378535be33c36bb", + "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 334458a9..f3174875 100644 --- a/flake.nix +++ b/flake.nix @@ -36,9 +36,9 @@ pkgs = common.pkgs; helix = pkgs.fetchgit { url = "https://github.com/helix-editor/helix.git"; - rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5"; + rev = "987d8e6dd66d65c2503cc81a3b9ea8787435839a"; fetchSubmodules = true; - sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ="; + sha256 = "sha256-GRJ0zMJva9upUatc89AeKYuLq73nxcxDPKDSgEcPASE="; }; in pkgs.runCommand prev.src.name { } '' From f2c73d156757b2567f306dcca4b94cf01b172d38 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sun, 12 Sep 2021 11:52:47 -0400 Subject: [PATCH 61/92] Update dark_plus error colour This was recently changed in VSCode. --- runtime/themes/dark_plus.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 7eeb3f95..c48a7e28 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -62,7 +62,7 @@ "ui.text.focus" = { fg = "#ffffff" } "warning" = { fg = "#cca700" } -"error" = { fg = "#f48771" } +"error" = { fg = "#ff1212" } "info" = { fg = "#75beff" } "hint" = { fg = "#eeeeeeb3" } From 32977ed34124a99af7b51057a6723203ce23c59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 9 Sep 2021 12:35:14 +0900 Subject: [PATCH 62/92] ui: Trigger recalculate_size per popup render so contents can readjust --- helix-term/src/ui/menu.rs | 76 +++++++++++++++++++++----------------- helix-term/src/ui/popup.rs | 21 +++++++---- helix-term/src/ui/text.rs | 20 +++++++--- 3 files changed, 72 insertions(+), 45 deletions(-) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 24dd3e61..dab0c34f 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -33,6 +33,8 @@ pub struct Menu { scroll: usize, size: (u16, u16), + viewport: (u16, u16), + recalculate: bool, } impl Menu { @@ -51,6 +53,8 @@ impl Menu { callback_fn: Box::new(callback_fn), scroll: 0, size: (0, 0), + viewport: (0, 0), + recalculate: true, }; // TODO: scoring on empty input should just use a fastpath @@ -83,6 +87,7 @@ impl Menu { // reset cursor position self.cursor = None; self.scroll = 0; + self.recalculate = true; } pub fn move_up(&mut self) { @@ -99,6 +104,41 @@ impl Menu { self.adjust_scroll(); } + fn recalculate_size(&mut self, viewport: (u16, u16)) { + let n = self + .options + .first() + .map(|option| option.row().cells.len()) + .unwrap_or_default(); + let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.row(); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + + acc + }); + let len = max_lens.iter().sum::() + n + 1; // +1: reserve some space for scrollbar + let width = len.min(viewport.0 as usize); + + self.widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + + let height = self.matches.len().min(10).min(viewport.1 as usize); + + self.size = (width as u16, height as u16); + + // adjust scroll offsets if size changed + self.adjust_scroll(); + self.recalculate = false; + } + fn adjust_scroll(&mut self) { let win_height = self.size.1 as usize; if let Some(cursor) = self.cursor { @@ -221,43 +261,13 @@ impl Component for Menu { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let n = self - .options - .first() - .map(|option| option.row().cells.len()) - .unwrap_or_default(); - let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.row(); - // maintain max for each column - for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { - let width = cell.content.width(); - if width > *acc { - *acc = width; - } - } - - acc - }); - let len = max_lens.iter().sum::() + n + 1; // +1: reserve some space for scrollbar - let width = len.min(viewport.0 as usize); - - self.widths = max_lens - .into_iter() - .map(|len| Constraint::Length(len as u16)) - .collect(); - - let height = self.options.len().min(10).min(viewport.1 as usize); - - self.size = (width as u16, height as u16); - - // adjust scroll offsets if size changed - self.adjust_scroll(); + if viewport != self.viewport || self.recalculate { + self.recalculate_size(viewport); + } Some(self.size) } - // TODO: required size should re-trigger when we filter items so we can draw a smaller menu - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let theme = &cx.editor.theme; let style = theme diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 846cefb8..1bab1eae 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -16,8 +16,6 @@ pub struct Popup { } impl Popup { - // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different - // rendering) pub fn new(contents: T) -> Self { Self { contents, @@ -38,20 +36,26 @@ impl Popup { let (width, height) = self.size; + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above + // -- make sure frame doesn't stick out of bounds let mut rel_x = position.col as u16; - let rel_y = position.row as u16; + let mut rel_y = position.row as u16; if viewport.width <= rel_x + width { rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - }; + } // TODO: be able to specify orientation preference. We want above for most popups, below // for menus/autocomplete. - if height <= rel_y { - (rel_x, rel_y.saturating_sub(height)) // position above point + if viewport.height > rel_y + height { + rel_y += 1 // position below point } else { - (rel_x, rel_y + 1) // position below point + rel_y = rel_y.saturating_sub(height) // position above point } + + (rel_x, rel_y) } pub fn get_size(&self) -> (u16, u16) { @@ -133,6 +137,9 @@ impl Component for Popup { } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { + // trigger required_size so we recalculate if the child changed + self.required_size((viewport.width, viewport.height)); + cx.scroll = Some(self.scroll); let (rel_x, rel_y) = self.get_rel_position(viewport, cx); diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index 65a75a4a..4641fae1 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -5,11 +5,17 @@ use helix_view::graphics::Rect; pub struct Text { contents: String, + size: (u16, u16), + viewport: (u16, u16), } impl Text { pub fn new(contents: String) -> Self { - Self { contents } + Self { + contents, + size: (0, 0), + viewport: (0, 0), + } } } impl Component for Text { @@ -24,9 +30,13 @@ impl Component for Text { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = tui::text::Text::from(self.contents.clone()); - let width = std::cmp::min(contents.width() as u16, viewport.0); - let height = std::cmp::min(contents.height() as u16, viewport.1); - Some((width, height)) + if viewport != self.viewport { + let contents = tui::text::Text::from(self.contents.clone()); + let width = std::cmp::min(contents.width() as u16, viewport.0); + let height = std::cmp::min(contents.height() as u16, viewport.1); + self.size = (width, height); + self.viewport = viewport; + } + Some(self.size) } } From 066367c0a4fcfc1e7c2e926171672afef26736b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 9 Sep 2021 14:32:37 +0900 Subject: [PATCH 63/92] fix: Need to reset set_byte_range in case cursor_ref is reused. --- helix-core/src/syntax.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 1afe0e25..93da869b 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -372,10 +372,8 @@ impl Syntax { let config_ref = unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) }; - // TODO: if reusing cursors this might need resetting - if let Some(range) = &range { - cursor_ref.set_byte_range(range.clone()); - } + // if reusing cursors & no range this resets to whole range + cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); let captures = cursor_ref .captures(query_ref, tree_ref.root_node(), RopeProvider(source)) From 1540b37f3455326f9b0052f137f9e565f936dc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 10 Sep 2021 18:24:34 +0900 Subject: [PATCH 64/92] lsp: Silence window/logMessage if -v isn't used --- helix-term/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index e21c5504..6206e6f2 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -373,7 +373,7 @@ impl Application { log::warn!("unhandled window/showMessage: {:?}", params); } Notification::LogMessage(params) => { - log::warn!("unhandled window/logMessage: {:?}", params); + log::info!("window/logMessage: {:?}", params); } Notification::ProgressMessage(params) => { let lsp::ProgressParams { token, value } = params; From 3e12b0099342be12db1db64e36ca4ff29613f122 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Mon, 13 Sep 2021 04:48:12 -0400 Subject: [PATCH 65/92] Add `no_op` command (#743) * Add `no_op` command * Document `no_op` in `remapping.md` --- book/src/remapping.md | 2 ++ helix-term/src/commands.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/book/src/remapping.md b/book/src/remapping.md index 3f25e364..81f45da3 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes | Null | `"null"` | | Escape | `"esc"` | +Keys can be disabled by binding them to the `no_op` command. + Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fb885740..c5409494 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -162,6 +162,7 @@ impl Command { #[rustfmt::skip] commands!( + no_op, "Do nothing", move_char_left, "Move left", move_char_right, "Move right", move_line_up, "Move up", @@ -361,6 +362,8 @@ impl PartialEq for Command { } } +fn no_op(_cx: &mut Context) {} + fn move_impl(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) where F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range, From 116e562ff652b54ba482143428bfbf4ac3fcd62e Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Mon, 13 Sep 2021 14:18:58 +0530 Subject: [PATCH 66/92] Document `diagnostic` theme scope (#751) --- book/src/themes.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index fe5259d5..a99e3a59 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -186,9 +186,9 @@ These scopes are used for theming the editor interface. | `ui.menu.selected` | | | `ui.selection` | For selections in the editing area | | `ui.selection.primary` | | -| `warning` | Diagnostics warning | -| `error` | Diagnostics error | -| `info` | Diagnostics info | -| `hint` | Diagnostics hint | - +| `warning` | Diagnostics warning (gutter) | +| `error` | Diagnostics error (gutter) | +| `info` | Diagnostics info (gutter) | +| `hint` | Diagnostics hint (gutter) | +| `diagnostic` | For text in editing area | From 2f32d1859dfc9f32b364779ef5fbb94322717906 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Sep 2021 09:21:24 +0900 Subject: [PATCH 67/92] build(deps): bump anyhow from 1.0.43 to 1.0.44 (#755) Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.43 to 1.0.44. - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.43...1.0.44) --- updated-dependencies: - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b6ff670..c0011a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "arc-swap" From 51b7f40da1e564d9ecb5263421306d5846d98566 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Sep 2021 09:21:35 +0900 Subject: [PATCH 68/92] build(deps): bump similar from 1.3.0 to 2.0.0 (#754) Bumps [similar](https://github.com/mitsuhiko/similar) from 1.3.0 to 2.0.0. - [Release notes](https://github.com/mitsuhiko/similar/releases) - [Changelog](https://github.com/mitsuhiko/similar/blob/main/CHANGELOG.md) - [Commits](https://github.com/mitsuhiko/similar/compare/1.3.0...2.0.0) --- updated-dependencies: - dependency-name: similar dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- helix-core/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0011a24..93b456a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,9 +875,9 @@ dependencies = [ [[package]] name = "similar" -version = "1.3.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" +checksum = "6bf11003835e462f07851028082d2a1c89d956180ce4b4b50e07fb085ec4131a" [[package]] name = "slab" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 5d2db642..2b963676 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -30,7 +30,7 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } toml = "0.5" -similar = "1.3" +similar = "2.0" etcetera = "0.3" From ef532e0c0df3e9f8bf4ac5af74b54f32b7ea2728 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Wed, 15 Sep 2021 01:58:06 -0400 Subject: [PATCH 69/92] log errors produced when trying to initialize the LSP (#746) --- helix-lsp/src/lib.rs | 10 +++++++++- helix-view/src/editor.rs | 12 ++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 7357c885..35cff754 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -318,7 +318,15 @@ impl Registry { let (client, incoming, initialize_notify) = Client::start( &config.command, &config.args, - serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), + serde_json::from_str(language_config.config.as_deref().unwrap_or("")) + .map_err(|e| { + log::error!( + "LSP Config, {}, in `languages.toml` for `{}`", + e, + language_config.scope() + ) + }) + .ok(), id, )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 52a0060c..a3d0d032 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -249,10 +249,14 @@ impl Editor { let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?; // try to find a language server based on the language name - let language_server = doc - .language - .as_ref() - .and_then(|language| self.language_servers.get(language).ok()); + let language_server = doc.language.as_ref().and_then(|language| { + self.language_servers + .get(language) + .map_err(|e| { + log::error!("Failed to get LSP, {}, for `{}`", e, language.scope()) + }) + .ok() + }); if let Some(language_server) = language_server { let language_id = doc From dd0b15e1f1540d6ca8c58594be302c66005d755c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 16 Sep 2021 15:47:51 +0900 Subject: [PATCH 70/92] syntax: Properly handle injection-regex for language injections --- helix-core/src/syntax.rs | 36 ++++++++++++++++++++++++++++++++++- helix-term/src/ui/editor.rs | 3 +-- helix-term/src/ui/markdown.rs | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 93da869b..8bbf363f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -21,6 +21,15 @@ use std::{ use once_cell::sync::{Lazy, OnceCell}; use serde::{Deserialize, Serialize}; +fn deserialize_regex<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Option::::deserialize(deserializer)? + .map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom)) + .transpose() +} + #[derive(Debug, Serialize, Deserialize)] pub struct Configuration { pub language: Vec, @@ -42,7 +51,8 @@ pub struct LanguageConfiguration { pub auto_format: bool, // content_regex - // injection_regex + #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] + injection_regex: Option, // first_line_regex // #[serde(skip)] @@ -243,6 +253,30 @@ impl Loader { .cloned() } + pub fn language_configuration_for_injection_string( + &self, + string: &str, + ) -> Option> { + let mut best_match_length = 0; + let mut best_match_position = None; + for (i, configuration) in self.language_configs.iter().enumerate() { + if let Some(injection_regex) = &configuration.injection_regex { + if let Some(mat) = injection_regex.find(string) { + let length = mat.end() - mat.start(); + if length > best_match_length { + best_match_position = Some(i); + best_match_length = length; + } + } + } + } + + if let Some(i) = best_match_position { + let configuration = &self.language_configs[i]; + return Some(configuration.clone()); + } + None + } pub fn language_configs_iter(&self) -> impl Iterator> { self.language_configs.iter() } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 52cf3d2b..0605e2c7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -147,8 +147,7 @@ impl EditorView { let scopes = theme.scopes(); syntax .highlight_iter(text.slice(..), Some(range), None, |language| { - loader - .language_config_for_scope(&format!("source.{}", language)) + loader.language_configuration_for_injection_string(language) .and_then(|language_config| { let config = language_config.highlight_config(scopes)?; let config_ref = config.as_ref(); diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 87b35a2d..4144ed3c 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -88,7 +88,7 @@ fn parse<'a>( if let Some(theme) = theme { let rope = Rope::from(text.as_ref()); let syntax = loader - .language_config_for_scope(&format!("source.{}", language)) + .language_configuration_for_injection_string(language) .and_then(|config| config.highlight_config(theme.scopes())) .map(|config| Syntax::new(&rope, config)); From d8b94ba85fdbed9fb0e9ebc60e19e6353efdbf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 16 Sep 2021 15:54:43 +0900 Subject: [PATCH 71/92] Fix broken test --- TODO.md | 4 ---- helix-core/src/indent.rs | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index f6a9ef09..d81cf302 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,8 @@ - tree sitter: - - lua - markdown - - zig - regex - - vue - kotlin - - julia - clojure - erlang diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 55802059..d9a0155f 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -454,6 +454,7 @@ where highlight_config: OnceCell::new(), config: None, // + injection_regex: None, roots: vec![], comment_token: None, auto_format: false, From 64e8f0017c9d8d8fd02b3e86378522974f6437b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 16 Sep 2021 16:04:32 +0900 Subject: [PATCH 72/92] ... --- helix-core/src/syntax.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 8bbf363f..547b2572 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -52,7 +52,7 @@ pub struct LanguageConfiguration { // content_regex #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] - injection_regex: Option, + pub injection_regex: Option, // first_line_regex // #[serde(skip)] From b2195e08b54d9e01411e3b67c87c93711415884c Mon Sep 17 00:00:00 2001 From: Raphael Megzari Date: Fri, 17 Sep 2021 11:04:55 +0900 Subject: [PATCH 73/92] languages: add svelte support (#733) * languages: add svelte support * languages: add svelte injections --- .gitmodules | 4 ++ languages.toml | 9 ++++ runtime/queries/svelte/highlights.scm | 68 +++++++++++++++++++++++++++ runtime/queries/svelte/indents.toml | 18 +++++++ runtime/queries/svelte/injections.scm | 30 ++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 runtime/queries/svelte/highlights.scm create mode 100644 runtime/queries/svelte/indents.toml create mode 100644 runtime/queries/svelte/injections.scm diff --git a/.gitmodules b/.gitmodules index f905b8c7..d1fc1517 100644 --- a/.gitmodules +++ b/.gitmodules @@ -118,3 +118,7 @@ path = helix-syntax/languages/tree-sitter-zig url = https://github.com/maxxnino/tree-sitter-zig shallow = true +[submodule "helix-syntax/languages/tree-sitter-svelte"] + path = helix-syntax/languages/tree-sitter-svelte + url = https://github.com/Himujjal/tree-sitter-svelte + shallow = true diff --git a/languages.toml b/languages.toml index 5c147770..fe473f89 100644 --- a/languages.toml +++ b/languages.toml @@ -277,6 +277,15 @@ roots = [] comment-token = "--" indent = { tab-width = 2, unit = " " } +[[language]] +name = "svelte" +scope = "source.svelte" +injection-regex = "svelte" +file-types = ["svelte"] +roots = [] +indent = { tab-width = 2, unit = " " } +language-server = { command = "svelteserver", args = ["--stdio"] } + [[language]] name = "yaml" scope = "source.yaml" diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm new file mode 100644 index 00000000..4c6f5f35 --- /dev/null +++ b/runtime/queries/svelte/highlights.scm @@ -0,0 +1,68 @@ +; Special identifiers +;-------------------- + +; TODO: +((element (start_tag (tag_name) @_tag) (text) @markup.heading) + (#match? @_tag "^(h[0-9]|title)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.bold) + (#match? @_tag "^(strong|b)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.italic) + (#match? @_tag "^(em|i)$")) + +; ((element (start_tag (tag_name) @_tag) (text) @markup.strike) +; (#match? @_tag "^(s|del)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.underline) + (#eq? @_tag "u")) + +((element (start_tag (tag_name) @_tag) (text) @markup.inline) + (#match? @_tag "^(code|kbd)$")) + +((element (start_tag (tag_name) @_tag) (text) @markup.underline.link) + (#eq? @_tag "a")) + +((attribute + (attribute_name) @_attr + (quoted_attribute_value (attribute_value) @markup.undeline.link)) + (#match? @_attr "^(href|src)$")) + +(tag_name) @tag +(attribute_name) @property +(erroneous_end_tag_name) @error +(comment) @comment + +[ + (attribute_value) + (quoted_attribute_value) +] @string + +[ + (text) + (raw_text_expr) +] @none + +[ + (special_block_keyword) + (then) + (as) +] @keyword + +[ + "{" + "}" +] @punctuation.brackets + +"=" @operator + +[ + "<" + ">" + "" + "#" + ":" + "/" + "@" +] @punctuation.definition.tag diff --git a/runtime/queries/svelte/indents.toml b/runtime/queries/svelte/indents.toml new file mode 100644 index 00000000..693db8e3 --- /dev/null +++ b/runtime/queries/svelte/indents.toml @@ -0,0 +1,18 @@ +indent = [ + "element" + "if_statement" + "each_statement" + "await_statement" +] + +outdent = [ + "end_tag" + "else_statement" + "if_end_expr" + "each_end_expr" + "await_end_expr" + ">" + "/>" +] + +ignore = "comment" diff --git a/runtime/queries/svelte/injections.scm b/runtime/queries/svelte/injections.scm new file mode 100644 index 00000000..266f4701 --- /dev/null +++ b/runtime/queries/svelte/injections.scm @@ -0,0 +1,30 @@ +; injections.scm +; -------------- +((style_element + (raw_text) @injection.content) + (#set! injection.language "css")) + +((attribute + (attribute_name) @_attr + (quoted_attribute_value (attribute_value) @css)) + (#eq? @_attr "style")) + +((script_element + (raw_text) @injection.content) + (#set! injection.language "javascript")) + +((raw_text_expr) @injection.content + (#set! injection.language "javascript")) + +( + (script_element + (start_tag + (attribute + (quoted_attribute_value (attribute_value) @_lang))) + (raw_text) @injection.content) + (#match? @_lang "(ts|typescript)") + (#set! injection.language "typescript") +) + +(comment) @comment + From 07be66c6775dfee8614c3d0f61612eec98a6c180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 17 Sep 2021 11:03:29 +0900 Subject: [PATCH 74/92] Revert parameter underlining on default theme I like it, but it clashes with diagnostics underlines since we can't color them differently in the terminal. If undercurl support is sufficient enough I'd consider changing diagnostics to use that instead. --- theme.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theme.toml b/theme.toml index 49f46b0b..82b71a7d 100644 --- a/theme.toml +++ b/theme.toml @@ -9,7 +9,8 @@ special = "honey" property = "white" variable = "lavender" # variable = "almond" # TODO: metavariables only -"variable.parameter" = { fg = "lavender", modifiers = ["underlined"] } +# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] } +"variable.parameter" = { fg = "lavender" } "variable.builtin" = "mint" type = "white" "type.builtin" = "white" # TODO: distinguish? From b02d872938395566c82658c54a22449b2c968beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 17 Sep 2021 14:31:56 +0900 Subject: [PATCH 75/92] fix: Refactor apply_workspace_edit to remove assert Fixes #698 --- helix-term/src/commands.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c5409494..010a6986 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2445,7 +2445,7 @@ fn apply_workspace_edit( ) { if let Some(ref changes) = workspace_edit.changes { log::debug!("workspace changes: {:?}", changes); - editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); return; // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used // TODO: find some example that uses workspace changes, and test it @@ -2463,8 +2463,30 @@ fn apply_workspace_edit( match document_changes { lsp::DocumentChanges::Edits(document_edits) => { for document_edit in document_edits { - let (view, doc) = current!(editor); - assert_eq!(doc.url().unwrap(), document_edit.text_document.uri); + let path = document_edit + .text_document + .uri + .to_file_path() + .expect("unable to convert URI to filepath"); + let current_view_id = view!(editor).id; + let doc = editor + .document_by_path_mut(path) + .expect("Document for document_changes not found"); + + // Need to determine a view for apply/append_changes_to_history + let selections = doc.selections(); + let view_id = if selections.contains_key(¤t_view_id) { + // use current if possible + current_view_id + } else { + // Hack: we take the first available view_id + selections + .keys() + .next() + .copied() + .expect("No view_id available") + }; + let edits = document_edit .edits .iter() @@ -2482,8 +2504,8 @@ fn apply_workspace_edit( edits, offset_encoding, ); - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + doc.apply(&transaction, view_id); + doc.append_changes_to_history(view_id); } } lsp::DocumentChanges::Operations(operations) => { From c7d6e4461f249108189cddf44b487b4c71c5520a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 17 Sep 2021 14:34:59 +0900 Subject: [PATCH 76/92] fix: Wrap around the top of the picker menu when scrolling Forgot to port the improvements in menu.rs Fixes #734 --- helix-term/src/ui/picker.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e040e0ff..c5b90a9c 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -270,17 +270,15 @@ impl Picker { } pub fn move_up(&mut self) { - self.cursor = self.cursor.saturating_sub(1); + let len = self.matches.len(); + let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; + self.cursor = pos; } pub fn move_down(&mut self) { - if self.matches.is_empty() { - return; - } - - if self.cursor < self.matches.len() - 1 { - self.cursor += 1; - } + let len = self.matches.len(); + let pos = (self.cursor + 1) % len; + self.cursor = pos; } pub fn selection(&self) -> Option<&T> { From 3ff5b001ac721606b68a594958abeee8832a023e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 17 Sep 2021 14:42:14 +0900 Subject: [PATCH 77/92] fix: Don't allow closing the last split if there's unsaved changes Fixes #674 --- helix-term/src/commands.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 010a6986..d2a838ba 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1568,7 +1568,7 @@ mod cmd { /// Results an error if there are modified buffers remaining and sets editor error, /// otherwise returns `Ok(())` - fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { + pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { let modified: Vec<_> = editor .documents() .filter(|doc| doc.is_modified()) @@ -4157,6 +4157,12 @@ fn vsplit(cx: &mut Context) { } fn wclose(cx: &mut Context) { + if cx.editor.tree.views().count() == 1 { + if let Err(err) = cmd::buffers_remaining_impl(cx.editor) { + cx.editor.set_error(err.to_string()); + return; + } + } let view_id = view!(cx.editor).id; // close current split cx.editor.close(view_id, /* close_buffer */ false); From 1d04e5938daf178dbbcdb2249f42f8485047d4cf Mon Sep 17 00:00:00 2001 From: Leoi Hung Kin Date: Fri, 17 Sep 2021 16:22:17 +0800 Subject: [PATCH 78/92] search_next_impl: don't panic on invalid regex (#740) --- helix-term/src/commands.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d2a838ba..703b92d1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1154,8 +1154,15 @@ fn search_next_impl(cx: &mut Context, extend: bool) { if let Some(query) = registers.read('/') { let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - let regex = Regex::new(query).unwrap(); - search_impl(doc, view, &contents, ®ex, extend); + if let Ok(regex) = Regex::new(query) { + search_impl(doc, view, &contents, ®ex, extend); + } else { + // get around warning `mutable_borrow_reservation_conflict` + // which will be a hard error in the future + // see: https://github.com/rust-lang/rust/issues/59159 + let query = query.clone(); + cx.editor.set_error(format!("Invalid regex: {}", query)); + } } } From 70a20b7cf85a80deab69c753a72ffcb3acbdf0af Mon Sep 17 00:00:00 2001 From: Alex <3957610+CptPotato@users.noreply.github.com> Date: Fri, 17 Sep 2021 15:40:24 +0200 Subject: [PATCH 79/92] add everforest dark theme (#760) --- runtime/themes/everforest_dark.toml | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 runtime/themes/everforest_dark.toml diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml new file mode 100644 index 00000000..462c8265 --- /dev/null +++ b/runtime/themes/everforest_dark.toml @@ -0,0 +1,87 @@ +# Everforest (Dark Hard) +# Author: CptPotato + +# Original Author: +# URL: https://github.com/sainnhe/everforest +# Filename: autoload/everforest.vim +# Author: sainnhe +# Email: sainnhe@gmail.com +# License: MIT License + +"escape" = "orange" +"type" = "yellow" +"constant" = "purple" +"number" = "purple" +"string" = "grey2" +"comment" = "grey0" +"variable" = "fg" +"variable.builtin" = "blue" +"variable.parameter" = "fg" +"variable.property" = "fg" +"label" = "aqua" +"punctuation" = "grey2" +"punctuation.delimiter" = "grey2" +"punctuation.bracket" = "fg" +"keyword" = "red" +"operator" = "orange" +"function" = "green" +"function.builtin" = "blue" +"function.macro" = "aqua" +"tag" = "yellow" +"namespace" = "aqua" +"attribute" = "aqua" +"constructor" = "yellow" +"module" = "blue" +"property" = "fg" +"special" = "orange" + +"ui.background" = { bg = "bg0" } +"ui.cursor" = { fg = "bg0", bg = "fg" } +"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" } +"ui.cursor.insert" = { fg = "bg0", bg = "grey1" } +"ui.cursor.select" = { fg = "bg0", bg = "blue" } +"ui.linenr" = "grey0" +"ui.linenr.selected" = "fg" +"ui.statusline" = { fg = "grey2", bg = "bg2" } +"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" } +"ui.popup" = { fg = "grey2", bg = "bg1" } +"ui.window" = { fg = "grey2", bg = "bg1" } +"ui.help" = { fg = "fg", bg = "bg1" } +"ui.text" = "fg" +"ui.text.focus" = "fg" +"ui.menu" = { fg = "fg", bg = "bg2" } +"ui.menu.selected" = { fg = "bg0", bg = "green" } +"ui.selection" = { bg = "bg3" } + +"hint" = "blue" +"info" = "aqua" +"warning" = "yellow" +"error" = "red" +"diagnostic" = { modifiers = ["underlined"] } + + +[palette] + +bg0 = "#2b3339" +bg1 = "#323c41" +bg2 = "#3a454a" +bg3 = "#445055" +bg4 = "#4c555b" +bg5 = "#53605c" +bg_visual = "#503946" +bg_red = "#4e3e43" +bg_green = "#404d44" +bg_blue = "#394f5a" +bg_yellow = "#4a4940" + +fg = "#d3c6aa" +red = "#e67e80" +orange = "#e69875" +yellow = "#dbbc7f" +green = "#a7c080" +aqua = "#83c092" +blue = "#7fbbb3" +purple = "#d699b6" +grey0 = "#7a8478" +grey1 = "#859289" +grey2 = "#9da9a0" From ae4d37de28abab23651aa6a239e6d9d90674f039 Mon Sep 17 00:00:00 2001 From: Raphael Megzari Date: Sat, 18 Sep 2021 09:59:26 +0900 Subject: [PATCH 80/92] flake: remove hack to fix helix version (#762) --- flake.nix | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/flake.nix b/flake.nix index f3174875..bcc9383e 100644 --- a/flake.nix +++ b/flake.nix @@ -30,22 +30,7 @@ }; # link languages and theme toml files since helix-view expects them helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; }; - helix-syntax = prev: { - src = - let - pkgs = common.pkgs; - helix = pkgs.fetchgit { - url = "https://github.com/helix-editor/helix.git"; - rev = "987d8e6dd66d65c2503cc81a3b9ea8787435839a"; - fetchSubmodules = true; - sha256 = "sha256-GRJ0zMJva9upUatc89AeKYuLq73nxcxDPKDSgEcPASE="; - }; - in - pkgs.runCommand prev.src.name { } '' - mkdir -p $out - ln -s ${prev.src}/* $out - ln -sf ${helix}/helix-syntax/languages $out - ''; + helix-syntax = _prev: { preConfigure = "mkdir -p ../runtime/grammars"; postInstall = "cp -r ../runtime $out/runtime"; }; From e0e41f4f775db1210b5a8d5d224ac74c7756471c Mon Sep 17 00:00:00 2001 From: Raphael Megzari Date: Sun, 19 Sep 2021 11:55:15 +0900 Subject: [PATCH 81/92] languages: add svelte submodule reference (#766) --- helix-syntax/languages/tree-sitter-svelte | 1 + 1 file changed, 1 insertion(+) create mode 160000 helix-syntax/languages/tree-sitter-svelte diff --git a/helix-syntax/languages/tree-sitter-svelte b/helix-syntax/languages/tree-sitter-svelte new file mode 160000 index 00000000..349a5984 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-svelte @@ -0,0 +1 @@ +Subproject commit 349a5984513b4a4a9e143a6e746120c6ff6cf6ed From 4a003782a51a94259ef3b5ddfacb2a148c5056e7 Mon Sep 17 00:00:00 2001 From: kraem Date: Mon, 20 Sep 2021 06:45:07 +0200 Subject: [PATCH 82/92] enable smart case regex search by default (#761) --- TODO.md | 2 -- book/src/configuration.md | 1 + book/src/keymap.md | 3 +-- helix-term/src/commands.rs | 12 ++++++++++-- helix-term/src/ui/mod.rs | 12 +++++++++++- helix-view/src/editor.rs | 3 +++ 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index d81cf302..90e7e450 100644 --- a/TODO.md +++ b/TODO.md @@ -22,8 +22,6 @@ as you type completion! - [ ] lsp: signature help -- [ ] search: smart case by default: insensitive unless upper detected - 2 - [ ] macro recording - [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) diff --git a/book/src/configuration.md b/book/src/configuration.md index 5a28362d..90cdd8a7 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -17,6 +17,7 @@ To override global configuration parameters, create a `config.toml` file located | `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | | `line-number` | Line number display (`absolute`, `relative`) | `absolute` | +| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | ## LSP diff --git a/book/src/keymap.md b/book/src/keymap.md index 4fa5033d..16d2420d 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -104,8 +104,7 @@ ### Search -> TODO: The search implementation isn't ideal yet -- we don't support searching -in reverse, or searching via smartcase. +> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse. | Key | Description | Command | | ----- | ----------- | ------- | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 703b92d1..d40bb9cf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5,7 +5,7 @@ use helix_core::{ match_brackets, movement::{self, Direction}, object, pos_at_coords, - regex::{self, Regex}, + regex::{self, Regex, RegexBuilder}, register::Register, search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -1154,7 +1154,15 @@ fn search_next_impl(cx: &mut Context, extend: bool) { if let Some(query) = registers.read('/') { let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - if let Ok(regex) = Regex::new(query) { + let case_insensitive = if cx.editor.config.smart_case { + !query.chars().any(char::is_uppercase) + } else { + false + }; + if let Ok(regex) = RegexBuilder::new(query) + .case_insensitive(case_insensitive) + .build() + { search_impl(doc, view, &contents, ®ex, extend); } else { // get around warning `mutable_borrow_reservation_conflict` diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 07eef352..f6536eb2 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,6 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; use helix_core::regex::Regex; +use helix_core::regex::RegexBuilder; use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -53,7 +54,16 @@ pub fn regex_prompt( return; } - match Regex::new(input) { + let case_insensitive = if cx.editor.config.smart_case { + !input.chars().any(char::is_uppercase) + } else { + false + }; + + match RegexBuilder::new(input) + .case_insensitive(case_insensitive) + .build() + { Ok(regex) => { let (view, doc) = current!(cx.editor); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index a3d0d032..b7df4a9b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -39,6 +39,8 @@ pub struct Config { pub line_number: LineNumber, /// Middle click paste support. Defaults to true pub middle_click_paste: bool, + /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. + pub smart_case: bool, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -64,6 +66,7 @@ impl Default for Config { }, line_number: LineNumber::Absolute, middle_click_paste: true, + smart_case: true, } } } From 589f0acec5b85222190762ec8c673a9abe421545 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Sep 2021 10:01:37 +0900 Subject: [PATCH 83/92] build(deps): bump arc-swap from 1.3.2 to 1.4.0 (#772) Bumps [arc-swap](https://github.com/vorner/arc-swap) from 1.3.2 to 1.4.0. - [Release notes](https://github.com/vorner/arc-swap/releases) - [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md) - [Commits](https://github.com/vorner/arc-swap/compare/v1.3.2...v1.4.0) --- updated-dependencies: - dependency-name: arc-swap dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93b456a4..df57acef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "arc-swap" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ab7d9e73059c86c36473f459b52adbd99c3554a4fec492caef460806006f00" +checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63" [[package]] name = "autocfg" From 5b4ae7c7b6112902cd875c865d0764aff9834de8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Sep 2021 10:01:45 +0900 Subject: [PATCH 84/92] build(deps): bump unicode-width from 0.1.8 to 0.1.9 (#771) Bumps [unicode-width](https://github.com/unicode-rs/unicode-width) from 0.1.8 to 0.1.9. - [Release notes](https://github.com/unicode-rs/unicode-width/releases) - [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.8...v0.1.9) --- updated-dependencies: - dependency-name: unicode-width dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df57acef..00db3f0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1080,9 +1080,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" From a512f48e4574f7465dceecb89c4dbbfa9e21ff6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Sep 2021 10:01:53 +0900 Subject: [PATCH 85/92] build(deps): bump serde_json from 1.0.67 to 1.0.68 (#770) Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.67 to 1.0.68. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.67...v1.0.68) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00db3f0c..858c374b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,9 +811,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", From 9456d5c1a258e71bbb7e391dec8c3efb819e2d7d Mon Sep 17 00:00:00 2001 From: Leoi Hung Kin Date: Wed, 22 Sep 2021 00:03:12 +0800 Subject: [PATCH 86/92] Initial implementation of global search (#651) * initial implementation of global search * use tokio::sync::mpsc::unbounded_channel instead of Arc, Mutex, Waker poll_fn * use tokio_stream::wrappers::UnboundedReceiverStream to collect all search matches * regex_prompt: unified callback; refactor * global search doc --- Cargo.lock | 74 +++++++++++++++ book/src/keymap.md | 4 +- helix-term/Cargo.toml | 5 + helix-term/src/commands.rs | 188 ++++++++++++++++++++++++++++++++----- helix-term/src/keymap.rs | 1 + helix-term/src/ui/mod.rs | 12 ++- 6 files changed, 259 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 858c374b..2f586cb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,17 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" dependencies = [ + "lazy_static", "memchr", + "regex-automata", ] +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + [[package]] name = "bytes" version = "1.0.1" @@ -174,6 +182,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + [[package]] name = "error-code" version = "2.3.0" @@ -300,6 +317,45 @@ dependencies = [ "regex", ] +[[package]] +name = "grep-matcher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-regex" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06" +dependencies = [ + "aho-corasick", + "bstr", + "grep-matcher", + "log", + "regex", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "grep-searcher" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d" +dependencies = [ + "bstr", + "bytecount", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memmap2", +] + [[package]] name = "helix-core" version = "0.4.1" @@ -361,6 +417,8 @@ dependencies = [ "fern", "futures-util", "fuzzy-matcher", + "grep-regex", + "grep-searcher", "helix-core", "helix-lsp", "helix-tui", @@ -375,6 +433,7 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "tokio", + "tokio-stream", "toml", ] @@ -552,6 +611,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memmap2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" +dependencies = [ + "libc", +] + [[package]] name = "mio" version = "0.7.13" @@ -753,6 +821,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" diff --git a/book/src/keymap.md b/book/src/keymap.md index 16d2420d..5928a1ae 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -206,8 +206,10 @@ This layer is a kludge of mappings, mostly pickers. | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | +| `/` | Global search in workspace folder | `global_search` | - +> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file. + #### Unimpaired Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 57d592cc..fe4da96e 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -55,5 +55,10 @@ toml = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +# ripgrep for global search +grep-regex = "0.1.9" +grep-searcher = "0.1.8" +tokio-stream = "0.1.7" + [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d40bb9cf..5005962f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -31,7 +31,7 @@ use crate::{ }; use crate::job::{self, Job, Jobs}; -use futures_util::FutureExt; +use futures_util::{FutureExt, StreamExt}; use std::num::NonZeroUsize; use std::{fmt, future::Future}; @@ -43,6 +43,11 @@ use std::{ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; +use grep_regex::RegexMatcher; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; +use ignore::{DirEntry, WalkBuilder, WalkState}; +use tokio_stream::wrappers::UnboundedReceiverStream; + pub struct Context<'a> { pub register: Option, pub count: Option, @@ -209,6 +214,7 @@ impl Command { search_next, "Select next search match", extend_search_next, "Add next search match to selection", search_selection, "Use current selection as search pattern", + global_search, "Global Search in workspace folder", extend_line, "Select current line, if already selected, extend to next line", extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)", delete_selection, "Delete selection", @@ -1061,24 +1067,41 @@ fn select_all(cx: &mut Context) { fn select_regex(cx: &mut Context) { let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt(cx, "select:".into(), Some(reg), move |view, doc, regex| { - let text = doc.text().slice(..); - if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex) - { - doc.set_selection(view.id, selection); - } - }); + let prompt = ui::regex_prompt( + cx, + "select:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + if let Some(selection) = + selection::select_on_matches(text, doc.selection(view.id), ®ex) + { + doc.set_selection(view.id, selection); + } + }, + ); cx.push_layer(Box::new(prompt)); } fn split_selection(cx: &mut Context) { let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt(cx, "split:".into(), Some(reg), move |view, doc, regex| { - let text = doc.text().slice(..); - let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); - doc.set_selection(view.id, selection); - }); + let prompt = ui::regex_prompt( + cx, + "split:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); + doc.set_selection(view.id, selection); + }, + ); cx.push_layer(Box::new(prompt)); } @@ -1141,9 +1164,17 @@ fn search(cx: &mut Context) { // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); - let prompt = ui::regex_prompt(cx, "search:".into(), Some(reg), move |view, doc, regex| { - search_impl(doc, view, &contents, ®ex, false); - }); + let prompt = ui::regex_prompt( + cx, + "search:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + search_impl(doc, view, &contents, ®ex, false); + }, + ); cx.push_layer(Box::new(prompt)); } @@ -1192,6 +1223,111 @@ fn search_selection(cx: &mut Context) { cx.editor.set_status(msg); } +fn global_search(cx: &mut Context) { + let (all_matches_sx, all_matches_rx) = + tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); + let prompt = ui::regex_prompt( + cx, + "global search:".into(), + None, + move |_view, _doc, regex, event| { + if event != PromptEvent::Validate { + return; + } + if let Ok(matcher) = RegexMatcher::new_line_matcher(regex.as_str()) { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + + let search_root = std::env::current_dir() + .expect("Global search error: Failed to get current dir"); + WalkBuilder::new(search_root).build_parallel().run(|| { + let mut searcher_cl = searcher.clone(); + let matcher_cl = matcher.clone(); + let all_matches_sx_cl = all_matches_sx.clone(); + Box::new(move |dent: Result| -> WalkState { + let dent = match dent { + Ok(dent) => dent, + Err(_) => return WalkState::Continue, + }; + + match dent.file_type() { + Some(fi) => { + if !fi.is_file() { + return WalkState::Continue; + } + } + None => return WalkState::Continue, + } + + let result_sink = sinks::UTF8(|line_num, _| { + match all_matches_sx_cl + .send((line_num as usize - 1, dent.path().to_path_buf())) + { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + }); + let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink); + + if let Err(err) = result { + log::error!("Global search error: {}, {}", dent.path().display(), err); + } + WalkState::Continue + }) + }); + } else { + // Otherwise do nothing + // log::warn!("Global Search Invalid Pattern") + } + }, + ); + + cx.push_layer(Box::new(prompt)); + + let show_picker = async move { + let all_matches: Vec<(usize, PathBuf)> = + UnboundedReceiverStream::new(all_matches_rx).collect().await; + let call: job::Callback = + Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + if all_matches.is_empty() { + editor.set_status("No matches found".to_string()); + return; + } + let picker = FilePicker::new( + all_matches, + move |(_line_num, path)| path.to_str().unwrap().into(), + move |editor: &mut Editor, (line_num, path), action| { + match editor.open(path.into(), action) { + Ok(_) => {} + Err(e) => { + editor.set_error(format!( + "Failed to open file '{}': {}", + path.display(), + e + )); + return; + } + } + + let line_num = *line_num; + let (view, doc) = current!(editor); + let text = doc.text(); + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); + + doc.set_selection(view.id, Selection::single(start, end)); + align_view(doc, view, Align::Center); + }, + |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))), + ); + compositor.push(Box::new(picker)); + }); + Ok(call) + }; + cx.jobs.callback(show_picker); +} + fn extend_line(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -3847,13 +3983,21 @@ fn join_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) { // keep selections matching regex let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt(cx, "keep:".into(), Some(reg), move |view, doc, regex| { - let text = doc.text().slice(..); + let prompt = ui::regex_prompt( + cx, + "keep:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); - if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { - doc.set_selection(view.id, selection); - } - }); + if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { + doc.set_selection(view.id, selection); + } + }, + ); cx.push_layer(Box::new(prompt)); } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f38c8a40..f9bfcc50 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -555,6 +555,7 @@ impl Default for Keymaps { "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, "space" => keep_primary_selection, + "/" => global_search, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f6536eb2..810a9966 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,7 +29,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - fun: impl Fn(&mut View, &mut Document, Regex) + 'static, + fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); let view_id = view.id; @@ -47,6 +47,14 @@ pub fn regex_prompt( } PromptEvent::Validate => { // TODO: push_jump to store selection just before jump + + match Regex::new(input) { + Ok(regex) => { + let (view, doc) = current!(cx.editor); + fun(view, doc, regex, event); + } + Err(_err) => (), // TODO: mark command line as error + } } PromptEvent::Update => { // skip empty input, TODO: trigger default @@ -70,7 +78,7 @@ pub fn regex_prompt( // revert state to what it was before the last update doc.set_selection(view.id, snapshot.clone()); - fun(view, doc, regex); + fun(view, doc, regex, event); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); } From 432bec10eddb3f51f3a6e32aedbfd566d74cde75 Mon Sep 17 00:00:00 2001 From: Leoi Hung Kin Date: Fri, 24 Sep 2021 09:27:16 +0800 Subject: [PATCH 87/92] allow smart case in global search (#781) --- helix-term/src/commands.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5005962f..ac93b5d0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -43,7 +43,7 @@ use std::{ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; -use grep_regex::RegexMatcher; +use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -1226,6 +1226,7 @@ fn search_selection(cx: &mut Context) { fn global_search(cx: &mut Context) { let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); + let smart_case = cx.editor.config.smart_case; let prompt = ui::regex_prompt( cx, "global search:".into(), @@ -1234,7 +1235,11 @@ fn global_search(cx: &mut Context) { if event != PromptEvent::Validate { return; } - if let Ok(matcher) = RegexMatcher::new_line_matcher(regex.as_str()) { + + if let Ok(matcher) = RegexMatcherBuilder::new() + .case_smart(smart_case) + .build(regex.as_str()) + { let searcher = SearcherBuilder::new() .binary_detection(BinaryDetection::quit(b'\x00')) .build(); From a958d34bfbcf45c01ce0d9c0d76e681fb863fc6a Mon Sep 17 00:00:00 2001 From: lurpahi <91169847+lurpahi@users.noreply.github.com> Date: Thu, 23 Sep 2021 18:28:44 -0700 Subject: [PATCH 88/92] Add option for automatic insertion of closing-parens/brackets/etc (#779) * Add auto-pair editor option * Document auto-pair editor option * Make cargo fmt happy * Actually make cargo fmt happy * Rename auto-pair option to auto-pairs * Inline a few constants Co-authored-by: miaomai --- book/src/configuration.md | 1 + helix-term/src/commands.rs | 11 +++++++---- helix-view/src/editor.rs | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 90cdd8a7..60b12bfd 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -18,6 +18,7 @@ To override global configuration parameters, create a `config.toml` file located | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | | `line-number` | Line number display (`absolute`, `relative`) | `absolute` | | `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | +| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | ## LSP diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ac93b5d0..117ba046 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3371,17 +3371,20 @@ pub mod insert { } use helix_core::auto_pairs; - const HOOKS: &[Hook] = &[auto_pairs::hook, insert]; - const POST_HOOKS: &[PostHook] = &[completion, signature_help]; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current!(cx.editor); + let hooks: &[Hook] = match cx.editor.config.auto_pairs { + true => &[auto_pairs::hook, insert], + false => &[insert], + }; + let text = doc.text(); let selection = doc.selection(view.id).clone().cursors(text.slice(..)); // run through insert hooks, stopping on the first one that returns Some(t) - for hook in HOOKS { + for hook in hooks { if let Some(transaction) = hook(text, &selection, c) { doc.apply(&transaction, view.id); break; @@ -3391,7 +3394,7 @@ pub mod insert { // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // this could also generically look at Transaction, but it's a bit annoying to look at // Operation instead of Change. - for hook in POST_HOOKS { + for hook in &[completion, signature_help] { hook(cx, c); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index b7df4a9b..b08a2df2 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -41,6 +41,8 @@ pub struct Config { pub middle_click_paste: bool, /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. pub smart_case: bool, + /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. + pub auto_pairs: bool, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -67,6 +69,7 @@ impl Default for Config { line_number: LineNumber::Absolute, middle_click_paste: true, smart_case: true, + auto_pairs: true, } } } From 9ea9e779b2eef293c14ae50d5767035c0a9544a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 8 Sep 2021 16:53:10 +0900 Subject: [PATCH 89/92] experiment: Move keep_primary_selection to , --- book/src/keymap.md | 3 +-- helix-term/src/commands.rs | 4 ++++ helix-term/src/keymap.rs | 5 +---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 5928a1ae..aed48d5b 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -86,6 +86,7 @@ | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` | +| `,` | Keep only the primary selection | `keep_primary_selection` | | `C` | Copy selection onto the next line | `copy_selection_on_next_line` | | `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` | | `(` | Rotate main selection forward | `rotate_selections_backward` | @@ -99,7 +100,6 @@ | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | -| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | ### Search @@ -200,7 +200,6 @@ This layer is a kludge of mappings, mostly pickers. | `a` | Apply code action | `code_action` | | `'` | Open last fuzzy picker | `last_picker` | | `w` | Enter [window mode](#window-mode) | N/A | -| `space` | Keep primary selection TODO: it's here because space mode replaced it | `keep_primary_selection` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 117ba046..e3c351f6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2825,6 +2825,10 @@ fn open_above(cx: &mut Context) { fn normal_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); + if doc.mode == Mode::Normal { + return; + } + doc.mode = Mode::Normal; doc.append_changes_to_history(view.id); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f9bfcc50..a83b960e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -436,7 +436,6 @@ impl Default for Keymaps { "A" => append_to_line, "o" => open_below, "O" => open_above, - // [ ] equivalents too (add blank new line, no edit) "d" => delete_selection, // TODO: also delete without yanking @@ -500,8 +499,7 @@ impl Default for Keymaps { "K" => keep_selections, // TODO: and another method for inverse - // TODO: clashes with space mode - "space" => keep_primary_selection, + "," => keep_primary_selection, // "q" => record_macro, // "Q" => replay_macro, @@ -554,7 +552,6 @@ impl Default for Keymaps { "p" => paste_clipboard_after, "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, - "space" => keep_primary_selection, "/" => global_search, }, "z" => { "View" From 75dba1f9560c6ea579e79ff074e60ba2fb87ca63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 8 Sep 2021 17:21:10 +0900 Subject: [PATCH 90/92] experiment: space+k for LSP doc, K for keep_selections --- book/src/keymap.md | 4 ++-- helix-term/src/keymap.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index aed48d5b..78bac0cf 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -38,7 +38,6 @@ | `Z` | Enter sticky [view mode](#view-mode) | N/A | | `Ctrl-w` | Enter [window mode](#window-mode) | N/A | | `Space` | Enter [space mode](#space-mode) | N/A | -| `K` | Show documentation for the item under the cursor | `hover` | ### Changes @@ -98,7 +97,7 @@ | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | -| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | +| `K` | Keep selections matching the regex | `keep_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | @@ -194,6 +193,7 @@ This layer is a kludge of mappings, mostly pickers. | Key | Description | Command | | ----- | ----------- | ------- | +| `k` | Show documentation for the item under the cursor | `hover` | | `f` | Open file picker | `file_picker` | | `b` | Open buffer picker | `buffer_picker` | | `s` | Open symbol picker (current document) | `symbol_picker` | diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a83b960e..4343a0b6 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -495,7 +495,6 @@ impl Default for Keymaps { "<" => unindent, "=" => format_selections, "J" => join_selections, - // TODO: conflicts hover/doc "K" => keep_selections, // TODO: and another method for inverse @@ -527,7 +526,6 @@ impl Default for Keymaps { // move under c "C-c" => toggle_comments, - "K" => hover, // z family for save/restore/combine from/to sels from register @@ -553,6 +551,7 @@ impl Default for Keymaps { "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, "/" => global_search, + "k" => hover, }, "z" => { "View" "z" | "c" => align_view_center, From 2e0803c8d9ec0028c0d018be251c7c2b781247b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 22 Sep 2021 00:51:49 +0900 Subject: [PATCH 91/92] Implement 'remove_primary_selection' as Alt-, This allows removing search matches from the selection Fixes #713 --- helix-core/src/selection.rs | 9 +++++++++ helix-term/src/commands.rs | 17 +++++++++++++++++ helix-term/src/keymap.rs | 1 + 3 files changed, 27 insertions(+) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index a3ea2ed4..755ee679 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -360,6 +360,15 @@ impl Selection { self.normalize() } + /// Adds a new range to the selection and makes it the primary range. + pub fn remove(mut self, index: usize) -> Self { + self.ranges.remove(index); + if index < self.primary_index || self.primary_index == self.ranges.len() { + self.primary_index -= 1; + } + self + } + /// Map selections over a set of changes. Useful for adjusting the selection position after /// applying changes to a document. pub fn map(self, changes: &ChangeSet) -> Self { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e3c351f6..025639a5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -290,6 +290,7 @@ impl Command { join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", keep_primary_selection, "Keep primary selection", + remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", hover, "Show docs for item under cursor", toggle_comments, "Comment/uncomment selections", @@ -4016,11 +4017,27 @@ fn keep_selections(cx: &mut Context) { fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); + // TODO: handle count let range = doc.selection(view.id).primary(); doc.set_selection(view.id, Selection::single(range.anchor, range.head)); } +fn remove_primary_selection(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + // TODO: handle count + + let selection = doc.selection(view.id); + if selection.len() == 1 { + cx.editor.set_error("no selections remaining".to_owned()); + return; + } + let index = selection.primary_index(); + let selection = selection.clone().remove(index); + + doc.set_selection(view.id, selection); +} + fn completion(cx: &mut Context) { // trigger on trigger char, or if user calls it // (or on word char typing??) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4343a0b6..cd4d3a32 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -499,6 +499,7 @@ impl Default for Keymaps { // TODO: and another method for inverse "," => keep_primary_selection, + "A-," => remove_primary_selection, // "q" => record_macro, // "Q" => replay_macro, From df55eaae69d0388de26448e82f9ded483fca2f44 Mon Sep 17 00:00:00 2001 From: Matt W Date: Thu, 23 Sep 2021 19:21:04 -0700 Subject: [PATCH 92/92] Add tilde expansion for file opening (#782) * change to helix_core's tilde expansion, from helix-core::path::expand_tilde --- helix-term/src/commands.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 025639a5..26f599bd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1524,8 +1524,11 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { + use helix_core::path::expand_tilde; let path = args.get(0).context("wrong argument count")?; - let _ = cx.editor.open(path.into(), Action::Replace)?; + let _ = cx + .editor + .open(expand_tilde(Path::new(path)), Action::Replace)?; Ok(()) }