From 999b45b28c157418c20a9a8cd9219db6ce0beac7 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 9 Jul 2022 03:35:06 +0530 Subject: [PATCH 001/151] Support different kinds of underline rendering Adds four new modifiers that can be used in themes: - undercurled - underdashed - underdotted - double-underline --- helix-tui/src/backend/crossterm.rs | 14 +++++++++++- helix-view/src/graphics.rs | 34 +++++++++++++++++++++--------- runtime/themes/onedark.toml | 2 +- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index eff098b35c8c9..252036f387c94 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -153,7 +153,7 @@ impl ModifierDiff { if removed.contains(Modifier::ITALIC) { map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; } - if removed.contains(Modifier::UNDERLINED) { + if removed.intersects(Modifier::ANY_UNDERLINE) { map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?; } if removed.contains(Modifier::DIM) { @@ -179,6 +179,18 @@ impl ModifierDiff { if added.contains(Modifier::UNDERLINED) { map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; } + if added.contains(Modifier::UNDERCURLED) { + map_error(queue!(w, SetAttribute(CAttribute::Undercurled)))?; + } + if added.contains(Modifier::UNDERDOTTED) { + map_error(queue!(w, SetAttribute(CAttribute::Underdotted)))?; + } + if added.contains(Modifier::UNDERDASHED) { + map_error(queue!(w, SetAttribute(CAttribute::Underdashed)))?; + } + if added.contains(Modifier::DOUBLE_UNDERLINED) { + map_error(queue!(w, SetAttribute(CAttribute::DoubleUnderlined)))?; + } if added.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index fb3c8b3f99426..15492119b5409 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -327,17 +327,27 @@ bitflags! { /// /// let m = Modifier::BOLD | Modifier::ITALIC; /// ``` - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case"))] pub struct Modifier: u16 { - const BOLD = 0b0000_0000_0001; - const DIM = 0b0000_0000_0010; - const ITALIC = 0b0000_0000_0100; - const UNDERLINED = 0b0000_0000_1000; - const SLOW_BLINK = 0b0000_0001_0000; - const RAPID_BLINK = 0b0000_0010_0000; - const REVERSED = 0b0000_0100_0000; - const HIDDEN = 0b0000_1000_0000; - const CROSSED_OUT = 0b0001_0000_0000; + const BOLD = 0b0000_0000_0000_0001; + const DIM = 0b0000_0000_0000_0010; + const ITALIC = 0b0000_0000_0000_0100; + const UNDERLINED = 0b0000_0000_0000_1000; + const SLOW_BLINK = 0b0000_0000_0001_0000; + const RAPID_BLINK = 0b0000_0000_0010_0000; + const REVERSED = 0b0000_0000_0100_0000; + const HIDDEN = 0b0000_0000_1000_0000; + const CROSSED_OUT = 0b0000_0001_0000_0000; + const UNDERCURLED = 0b0000_0010_0000_0000; + const UNDERDOTTED = 0b0000_0100_0000_0000; + const UNDERDASHED = 0b0000_1000_0000_0000; + const DOUBLE_UNDERLINED = 0b0001_0000_0000_0000; + + const ANY_UNDERLINE = Self::UNDERLINED.bits + | Self::UNDERCURLED.bits + | Self::UNDERDOTTED.bits + | Self::UNDERDASHED.bits + | Self::DOUBLE_UNDERLINED.bits; } } @@ -355,6 +365,10 @@ impl FromStr for Modifier { "reversed" => Ok(Self::REVERSED), "hidden" => Ok(Self::HIDDEN), "crossed_out" => Ok(Self::CROSSED_OUT), + "undercurled" => Ok(Self::UNDERCURLED), + "underdotted" => Ok(Self::UNDERDOTTED), + "underdashed" => Ok(Self::UNDERDASHED), + "double_underlined" => Ok(Self::DOUBLE_UNDERLINED), _ => Err("Invalid modifier"), } } diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 1e7d9af1f19a6..a4cc12eb9ef2a 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -39,7 +39,7 @@ "diff.delta" = "gold" "diff.minus" = "red" -diagnostic = { modifiers = ["underlined"] } +diagnostic = { modifiers = ["undercurled"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } From 3ad7d543ca17963f0839b1a6cd8abacdb5c60cf7 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Thu, 11 Aug 2022 14:10:29 +0300 Subject: [PATCH 002/151] Add separate color for underlines --- helix-tui/src/backend/crossterm.rs | 8 +++++++- helix-tui/src/buffer.rs | 7 +++++++ helix-view/src/graphics.rs | 19 +++++++++++++++++++ helix-view/src/theme.rs | 1 + runtime/themes/dark_plus.toml | 3 ++- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 252036f387c94..fe9da919876ec 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -4,7 +4,7 @@ use crossterm::{ execute, queue, style::{ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, - SetForegroundColor, + SetForegroundColor, SetUnderlineColor, }, terminal::{self, Clear, ClearType}, }; @@ -47,6 +47,7 @@ where { let mut fg = Color::Reset; let mut bg = Color::Reset; + let mut underline = Color::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<(u16, u16)> = None; for (x, y, cell) in content { @@ -73,6 +74,11 @@ where map_error(queue!(self.buffer, SetBackgroundColor(color)))?; bg = cell.bg; } + if cell.underline != underline { + let color = CColor::from(cell.underline); + map_error(queue!(self.buffer, SetUnderlineColor(color)))?; + underline = cell.underline; + } map_error(queue!(self.buffer, Print(&cell.symbol)))?; } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 21c53aadf9bc6..14f3ecafabb20 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -11,6 +11,7 @@ pub struct Cell { pub symbol: String, pub fg: Color, pub bg: Color, + pub underline: Color, pub modifier: Modifier, } @@ -44,6 +45,9 @@ impl Cell { if let Some(c) = style.bg { self.bg = c; } + if let Some(c) = style.underline { + self.underline = c; + } self.modifier.insert(style.add_modifier); self.modifier.remove(style.sub_modifier); self @@ -53,6 +57,7 @@ impl Cell { Style::default() .fg(self.fg) .bg(self.bg) + .underline(self.bg) .add_modifier(self.modifier) } @@ -61,6 +66,7 @@ impl Cell { self.symbol.push(' '); self.fg = Color::Reset; self.bg = Color::Reset; + self.underline = Color::Reset; self.modifier = Modifier::empty(); } } @@ -71,6 +77,7 @@ impl Default for Cell { symbol: " ".into(), fg: Color::Reset, bg: Color::Reset, + underline: Color::Reset, modifier: Modifier::empty(), } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 15492119b5409..c995f60c9512d 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -440,6 +440,7 @@ impl FromStr for Modifier { pub struct Style { pub fg: Option, pub bg: Option, + pub underline: Option, pub add_modifier: Modifier, pub sub_modifier: Modifier, } @@ -449,6 +450,7 @@ impl Default for Style { Style { fg: None, bg: None, + underline: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::empty(), } @@ -461,6 +463,7 @@ impl Style { Style { fg: Some(Color::Reset), bg: Some(Color::Reset), + underline: Some(Color::Reset), add_modifier: Modifier::empty(), sub_modifier: Modifier::all(), } @@ -496,6 +499,21 @@ impl Style { self } + /// Changes the underline color. + /// + /// ## Examples + /// + /// ```rust + /// # use helix_view::graphics::{Color, Style}; + /// let style = Style::default().underline(Color::Blue); + /// let diff = Style::default().underline(Color::Red); + /// assert_eq!(style.patch(diff), Style::default().underline(Color::Red)); + /// ``` + pub fn underline(mut self, color: Color) -> Style { + self.underline = Some(color); + self + } + /// Changes the text emphasis. /// /// When applied, it adds the given modifier to the `Style` modifiers. @@ -552,6 +570,7 @@ impl Style { pub fn patch(mut self, other: Style) -> Style { self.fg = other.fg.or(self.fg); self.bg = other.bg.or(self.bg); + self.underline = other.underline.or(self.underline); self.add_modifier.remove(other.sub_modifier); self.add_modifier.insert(other.add_modifier); diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index fa5fa702804b0..5ce1b2c579dfc 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -269,6 +269,7 @@ impl ThemePalette { match name.as_str() { "fg" => *style = style.fg(self.parse_color(value)?), "bg" => *style = style.bg(self.parse_color(value)?), + "underline" => *style = style.underline(self.parse_color(value)?), "modifiers" => { let modifiers = value .as_array() diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index f99da4fbfb259..d1a5756efd39a 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -92,7 +92,8 @@ "info" = { fg = "light_blue" } "hint" = { fg = "light_gray3" } -diagnostic = { modifiers = ["underlined"] } +"diagnostic.error" = {underline = "red", modifiers = ["undercurled"] } +"diagnostic" = {underline = "gold", modifiers = ["undercurled"] } [palette] white = "#ffffff" From 79a39c1063eca995bf75694743dc5eb3c905fa9c Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Sun, 4 Sep 2022 11:28:55 +0300 Subject: [PATCH 003/151] Fix failing tests Add underline field to doctests, and fix bugs --- helix-tui/src/buffer.rs | 5 +++-- helix-tui/src/text.rs | 4 ++++ helix-view/src/graphics.rs | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 14f3ecafabb20..3036608d5e5cc 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -57,7 +57,7 @@ impl Cell { Style::default() .fg(self.fg) .bg(self.bg) - .underline(self.bg) + .underline(self.underline) .add_modifier(self.modifier) } @@ -104,7 +104,8 @@ impl Default for Cell { /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, -/// modifier: Modifier::empty() +/// underline: Color::Reset, +/// modifier: Modifier::empty(), /// }); /// buf[(5, 0)].set_char('x'); /// assert_eq!(buf[(5, 0)].symbol, "x"); diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 602090e557efe..73d5880399f02 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -134,6 +134,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -143,6 +144,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -152,6 +154,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -161,6 +164,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index c995f60c9512d..6c854fd0629e1 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -405,6 +405,7 @@ impl FromStr for Modifier { /// fg: Some(Color::Yellow), /// bg: Some(Color::Red), /// add_modifier: Modifier::BOLD, +/// underline: Some(Color::Reset), /// sub_modifier: Modifier::empty(), /// }, /// buffer[(0, 0)].style(), @@ -429,6 +430,7 @@ impl FromStr for Modifier { /// Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Reset), +/// underline: Some(Color::Reset), /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, From de72b9c04caf1fe12fd6076646657c456403c229 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Sun, 4 Sep 2022 17:57:14 +0300 Subject: [PATCH 004/151] Update theme documentation --- book/src/themes.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 9908456fca38f..32ff2498f1ac4 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix Each line in the theme file is specified as below: ```toml -key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] } +key = { fg = "#ffffff", bg = "#000000", underline = "#ff0000", modifiers = ["bold", "italic", "undercurled"] } ``` -where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults. +where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline color (only meaningful if an underline modifier is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. To specify only the foreground color: @@ -77,17 +77,21 @@ 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` | +| Modifier | +| --- | +| `bold` | +| `dim` | +| `italic` | +| `underlined` | +| `undercurled` | +| `underdashed` | +| `underdotted` | +| `double-underlined` | +| `slow_blink` | +| `rapid_blink` | +| `reversed` | +| `hidden` | +| `crossed_out` | ### Scopes From 79d3d44c3db48365597eefd274e868bc1e15de57 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Wed, 7 Sep 2022 17:28:58 +0530 Subject: [PATCH 005/151] Detect extended underline support using terminfo The cxterminfo crate has been used over popular alternatives like `term` since it supports querying for extended capabilities and also for it's small codebase size (which will make it easy to inline it into helix in the future if required). --- Cargo.lock | 7 ++++ helix-tui/Cargo.toml | 1 + helix-tui/src/backend/crossterm.rs | 55 ++++++++++++++++++++++++++---- runtime/themes/onedark.toml | 5 ++- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5edcaac602f6..f980c4175a0b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,6 +176,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "cxterminfo" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da92c5e3aaf2cc1fea346d9b3bac0c59c6ffc1d1d46f18d991d449912a3e6f07" + [[package]] name = "dirs-next" version = "2.0.0" @@ -504,6 +510,7 @@ dependencies = [ "bitflags", "cassowary", "crossterm", + "cxterminfo", "helix-core", "helix-view", "serde", diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index b220c64f75b82..1c6a6a8d45b2c 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -20,6 +20,7 @@ bitflags = "1.3" cassowary = "0.3" unicode-segmentation = "1.10" crossterm = { version = "0.25", optional = true } +cxterminfo = "0.2" serde = { version = "1", "optional" = true, features = ["derive"]} helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-core = { version = "0.6", path = "../helix-core" } diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index fe9da919876ec..3a50074ede122 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -11,8 +11,38 @@ use crossterm::{ use helix_view::graphics::{Color, CursorKind, Modifier, Rect}; use std::io::{self, Write}; +fn vte_version() -> Option { + std::env::var("VTE_VERSION").ok()?.parse().ok() +} + +/// Describes terminal capabilities like extended underline, truecolor, etc. +#[derive(Copy, Clone, Debug, Default)] +struct Capabilities { + /// Support for undercurled, underdashed, etc. + has_extended_underlines: bool, +} + +impl Capabilities { + /// Detect capabilities from the terminfo database located based + /// on the $TERM environment variable. If detection fails, returns + /// a default value where no capability is supported. + pub fn from_env_or_default() -> Self { + match cxterminfo::terminfo::TermInfo::from_env() { + Err(_) => Capabilities::default(), + Ok(t) => Capabilities { + // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284 + // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines + has_extended_underlines: t.get_ext_string("Smulx").is_some() + || *t.get_ext_bool("Su").unwrap_or(&false) + || vte_version() >= Some(5102), + }, + } + } +} + pub struct CrosstermBackend { buffer: W, + capabilities: Capabilities, } impl CrosstermBackend @@ -20,7 +50,10 @@ where W: Write, { pub fn new(buffer: W) -> CrosstermBackend { - CrosstermBackend { buffer } + CrosstermBackend { + buffer, + capabilities: Capabilities::from_env_or_default(), + } } } @@ -61,7 +94,7 @@ where from: modifier, to: cell.modifier, }; - diff.queue(&mut self.buffer)?; + diff.queue(&mut self.buffer, self.capabilities)?; modifier = cell.modifier; } if cell.fg != fg { @@ -141,7 +174,7 @@ struct ModifierDiff { } impl ModifierDiff { - fn queue(&self, mut w: W) -> io::Result<()> + fn queue(&self, mut w: W, caps: Capabilities) -> io::Result<()> where W: io::Write, { @@ -172,6 +205,14 @@ impl ModifierDiff { map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; } + let queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> { + let underline = match caps.has_extended_underlines { + true => styled_underline, + false => CAttribute::Underlined, + }; + map_error(queue!(w, SetAttribute(underline))) + }; + let added = self.to - self.from; if added.contains(Modifier::REVERSED) { map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; @@ -186,16 +227,16 @@ impl ModifierDiff { map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; } if added.contains(Modifier::UNDERCURLED) { - map_error(queue!(w, SetAttribute(CAttribute::Undercurled)))?; + queue_styled_underline(CAttribute::Undercurled, &mut w)?; } if added.contains(Modifier::UNDERDOTTED) { - map_error(queue!(w, SetAttribute(CAttribute::Underdotted)))?; + queue_styled_underline(CAttribute::Underdotted, &mut w)?; } if added.contains(Modifier::UNDERDASHED) { - map_error(queue!(w, SetAttribute(CAttribute::Underdashed)))?; + queue_styled_underline(CAttribute::Underdashed, &mut w)?; } if added.contains(Modifier::DOUBLE_UNDERLINED) { - map_error(queue!(w, SetAttribute(CAttribute::DoubleUnderlined)))?; + queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?; } if added.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index a4cc12eb9ef2a..e2bc2c472d160 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -39,7 +39,10 @@ "diff.delta" = "gold" "diff.minus" = "red" -diagnostic = { modifiers = ["undercurled"] } +"diagnostic.info" = { underline = "blue", modifiers = ["undercurled"] } +"diagnostic.hint" = { underline = "green", modifiers = ["undercurled"] } +"diagnostic.warning" = { underline = "yellow", modifiers = ["undercurled"] } +"diagnostic.error" = { underline = "red", modifiers = ["undercurled"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } From 71ee589bbc723e7a55585ddc2ca43c29ee93fabe Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sat, 1 Oct 2022 02:15:25 +0200 Subject: [PATCH 006/151] make underline_style a seperate option Underline styles are mutally exclusive and overwrite each other. Therefore implementing as an modifier lead to incorrect behaviour when the underline style is overwritten. For backwards compatability the "underline" modified is retained (but deprecated). Instead the "underline_style" and "underline_color" optios should be used to style underlines. --- book/src/themes.md | 23 +++++-- helix-tui/src/backend/crossterm.rs | 56 +++++++--------- helix-tui/src/buffer.rs | 27 +++++--- helix-tui/src/text.rs | 12 ++-- helix-view/src/graphics.rs | 101 ++++++++++++++++++++--------- helix-view/src/gutter.rs | 4 +- helix-view/src/theme.rs | 23 ++++++- runtime/themes/dark_plus.toml | 4 +- runtime/themes/onedark.toml | 8 +-- 9 files changed, 167 insertions(+), 91 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 32ff2498f1ac4..450b3364958fa 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix Each line in the theme file is specified as below: ```toml -key = { fg = "#ffffff", bg = "#000000", underline = "#ff0000", modifiers = ["bold", "italic", "undercurled"] } +key = { fg = "#ffffff", bg = "#000000", underline_color = "#ff0000", underline_style = "curl", modifiers = ["bold", "italic"] } ``` -where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline color (only meaningful if an underline modifier is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. +where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline_style` the underline style, `underline_color` the underline color (only meaningful if an underline style is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. To specify only the foreground color: @@ -83,16 +83,27 @@ Less common modifiers might not be supported by your terminal emulator. | `dim` | | `italic` | | `underlined` | -| `undercurled` | -| `underdashed` | -| `underdotted` | -| `double-underlined` | | `slow_blink` | | `rapid_blink` | | `reversed` | | `hidden` | | `crossed_out` | +### Underline Style + +One of the following values may be used as an `underline_styles`. + +Some styles might not be supported by your terminal emulator. + +| Modifier | +| --- | +| `line` | +| `curl` | +| `dashed` | +| `dot` | +| `double-line` | + + ### Scopes The following is a list of scopes available to use for styling. diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 3a50074ede122..3e6dc5f59f4b6 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -8,7 +8,7 @@ use crossterm::{ }, terminal::{self, Clear, ClearType}, }; -use helix_view::graphics::{Color, CursorKind, Modifier, Rect}; +use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle}; use std::io::{self, Write}; fn vte_version() -> Option { @@ -80,7 +80,8 @@ where { let mut fg = Color::Reset; let mut bg = Color::Reset; - let mut underline = Color::Reset; + let mut underline_color = Color::Reset; + let mut underline_style = UnderlineStyle::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<(u16, u16)> = None; for (x, y, cell) in content { @@ -94,7 +95,7 @@ where from: modifier, to: cell.modifier, }; - diff.queue(&mut self.buffer, self.capabilities)?; + diff.queue(&mut self.buffer)?; modifier = cell.modifier; } if cell.fg != fg { @@ -107,10 +108,24 @@ where map_error(queue!(self.buffer, SetBackgroundColor(color)))?; bg = cell.bg; } - if cell.underline != underline { - let color = CColor::from(cell.underline); + if cell.underline_color != underline_color { + let color = CColor::from(cell.underline_color); map_error(queue!(self.buffer, SetUnderlineColor(color)))?; - underline = cell.underline; + underline_color = cell.underline_color; + } + + let mut new_underline_style = cell.underline_style; + if !self.capabilities.has_extended_underlines { + match new_underline_style { + UnderlineStyle::Reset => (), + _ => new_underline_style = UnderlineStyle::Line, + } + } + + if new_underline_style != underline_style { + let attr = CAttribute::from(cell.underline_style); + map_error(queue!(self.buffer, SetAttribute(attr)))?; + underline_style = new_underline_style; } map_error(queue!(self.buffer, Print(&cell.symbol)))?; @@ -118,6 +133,7 @@ where map_error(queue!( self.buffer, + SetUnderlineColor(CColor::Reset), SetForegroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset), SetAttribute(CAttribute::Reset) @@ -174,7 +190,7 @@ struct ModifierDiff { } impl ModifierDiff { - fn queue(&self, mut w: W, caps: Capabilities) -> io::Result<()> + fn queue(&self, mut w: W) -> io::Result<()> where W: io::Write, { @@ -192,9 +208,6 @@ impl ModifierDiff { if removed.contains(Modifier::ITALIC) { map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; } - if removed.intersects(Modifier::ANY_UNDERLINE) { - map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?; - } if removed.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; } @@ -205,14 +218,6 @@ impl ModifierDiff { map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; } - let queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> { - let underline = match caps.has_extended_underlines { - true => styled_underline, - false => CAttribute::Underlined, - }; - map_error(queue!(w, SetAttribute(underline))) - }; - let added = self.to - self.from; if added.contains(Modifier::REVERSED) { map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; @@ -223,21 +228,6 @@ impl ModifierDiff { if added.contains(Modifier::ITALIC) { map_error(queue!(w, SetAttribute(CAttribute::Italic)))?; } - if added.contains(Modifier::UNDERLINED) { - map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; - } - if added.contains(Modifier::UNDERCURLED) { - queue_styled_underline(CAttribute::Undercurled, &mut w)?; - } - if added.contains(Modifier::UNDERDOTTED) { - queue_styled_underline(CAttribute::Underdotted, &mut w)?; - } - if added.contains(Modifier::UNDERDASHED) { - queue_styled_underline(CAttribute::Underdashed, &mut w)?; - } - if added.contains(Modifier::DOUBLE_UNDERLINED) { - queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?; - } if added.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 3036608d5e5cc..424e6d321bd91 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -3,7 +3,7 @@ use helix_core::unicode::width::UnicodeWidthStr; use std::cmp::min; use unicode_segmentation::UnicodeSegmentation; -use helix_view::graphics::{Color, Modifier, Rect, Style}; +use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle}; /// A buffer cell #[derive(Debug, Clone, PartialEq)] @@ -11,7 +11,8 @@ pub struct Cell { pub symbol: String, pub fg: Color, pub bg: Color, - pub underline: Color, + pub underline_color: Color, + pub underline_style: UnderlineStyle, pub modifier: Modifier, } @@ -45,9 +46,13 @@ impl Cell { if let Some(c) = style.bg { self.bg = c; } - if let Some(c) = style.underline { - self.underline = c; + if let Some(c) = style.underline_color { + self.underline_color = c; } + if let Some(style) = style.underline_style { + self.underline_style = style; + } + self.modifier.insert(style.add_modifier); self.modifier.remove(style.sub_modifier); self @@ -57,7 +62,8 @@ impl Cell { Style::default() .fg(self.fg) .bg(self.bg) - .underline(self.underline) + .underline_color(self.underline_color) + .underline_style(self.underline_style) .add_modifier(self.modifier) } @@ -66,7 +72,8 @@ impl Cell { self.symbol.push(' '); self.fg = Color::Reset; self.bg = Color::Reset; - self.underline = Color::Reset; + self.underline_color = Color::Reset; + self.underline_style = UnderlineStyle::Reset; self.modifier = Modifier::empty(); } } @@ -77,7 +84,8 @@ impl Default for Cell { symbol: " ".into(), fg: Color::Reset, bg: Color::Reset, - underline: Color::Reset, + underline_color: Color::Reset, + underline_style: UnderlineStyle::Reset, modifier: Modifier::empty(), } } @@ -94,7 +102,7 @@ impl Default for Cell { /// /// ``` /// use helix_tui::buffer::{Buffer, Cell}; -/// use helix_view::graphics::{Rect, Color, Style, Modifier}; +/// use helix_view::graphics::{Rect, Color, UnderlineStyle, Style, Modifier}; /// /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5}); /// buf[(0, 2)].set_symbol("x"); @@ -104,7 +112,8 @@ impl Default for Cell { /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, -/// underline: Color::Reset, +/// underline_color: Color::Reset, +/// underline_style: UnderlineStyle::Reset, /// modifier: Modifier::empty(), /// }); /// buf[(5, 0)].set_char('x'); diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 73d5880399f02..1bfe5ee1f7572 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -134,7 +134,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), - /// underline: None, + /// underline_color: None, + /// underline_style: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -144,7 +145,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), - /// underline: None, + /// underline_color: None, + /// underline_style: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -154,7 +156,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), - /// underline: None, + /// underline_color: None, + /// underline_style: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -164,7 +167,8 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), - /// underline: None, + /// underline_color: None, + /// underline_style: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 6c854fd0629e1..01344748fd096 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -315,6 +315,44 @@ impl From for crossterm::style::Color { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UnderlineStyle { + Reset, + Line, + Curl, + Dotted, + Dashed, + DoubleLine, +} + +impl FromStr for UnderlineStyle { + type Err = &'static str; + + fn from_str(modifier: &str) -> Result { + match modifier { + "line" => Ok(Self::Line), + "curl" => Ok(Self::Curl), + "dotted" => Ok(Self::Dotted), + "dashed" => Ok(Self::Dashed), + "double_line" => Ok(Self::DoubleLine), + _ => Err("Invalid underline style"), + } + } +} + +impl From for crossterm::style::Attribute { + fn from(style: UnderlineStyle) -> Self { + match style { + UnderlineStyle::Line => crossterm::style::Attribute::Underlined, + UnderlineStyle::Curl => crossterm::style::Attribute::Undercurled, + UnderlineStyle::Dotted => crossterm::style::Attribute::Underdotted, + UnderlineStyle::Dashed => crossterm::style::Attribute::Underdashed, + UnderlineStyle::DoubleLine => crossterm::style::Attribute::DoubleUnderlined, + UnderlineStyle::Reset => crossterm::style::Attribute::NoUnderline, + } + } +} + bitflags! { /// Modifier changes the way a piece of text is displayed. /// @@ -332,22 +370,11 @@ bitflags! { const BOLD = 0b0000_0000_0000_0001; const DIM = 0b0000_0000_0000_0010; const ITALIC = 0b0000_0000_0000_0100; - const UNDERLINED = 0b0000_0000_0000_1000; const SLOW_BLINK = 0b0000_0000_0001_0000; const RAPID_BLINK = 0b0000_0000_0010_0000; const REVERSED = 0b0000_0000_0100_0000; const HIDDEN = 0b0000_0000_1000_0000; const CROSSED_OUT = 0b0000_0001_0000_0000; - const UNDERCURLED = 0b0000_0010_0000_0000; - const UNDERDOTTED = 0b0000_0100_0000_0000; - const UNDERDASHED = 0b0000_1000_0000_0000; - const DOUBLE_UNDERLINED = 0b0001_0000_0000_0000; - - const ANY_UNDERLINE = Self::UNDERLINED.bits - | Self::UNDERCURLED.bits - | Self::UNDERDOTTED.bits - | Self::UNDERDASHED.bits - | Self::DOUBLE_UNDERLINED.bits; } } @@ -359,16 +386,11 @@ impl FromStr for Modifier { "bold" => Ok(Self::BOLD), "dim" => Ok(Self::DIM), "italic" => Ok(Self::ITALIC), - "underlined" => Ok(Self::UNDERLINED), "slow_blink" => Ok(Self::SLOW_BLINK), "rapid_blink" => Ok(Self::RAPID_BLINK), "reversed" => Ok(Self::REVERSED), "hidden" => Ok(Self::HIDDEN), "crossed_out" => Ok(Self::CROSSED_OUT), - "undercurled" => Ok(Self::UNDERCURLED), - "underdotted" => Ok(Self::UNDERDOTTED), - "underdashed" => Ok(Self::UNDERDASHED), - "double_underlined" => Ok(Self::DOUBLE_UNDERLINED), _ => Err("Invalid modifier"), } } @@ -389,7 +411,7 @@ impl FromStr for Modifier { /// just S3. /// /// ```rust -/// # use helix_view::graphics::{Rect, Color, Modifier, Style}; +/// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style}; /// # use helix_tui::buffer::Buffer; /// let styles = [ /// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), @@ -405,7 +427,8 @@ impl FromStr for Modifier { /// fg: Some(Color::Yellow), /// bg: Some(Color::Red), /// add_modifier: Modifier::BOLD, -/// underline: Some(Color::Reset), +/// underline_color: Some(Color::Reset), +/// underline_style: Some(UnderlineStyle::Reset), /// sub_modifier: Modifier::empty(), /// }, /// buffer[(0, 0)].style(), @@ -416,7 +439,7 @@ impl FromStr for Modifier { /// reset all properties until that point use [`Style::reset`]. /// /// ``` -/// # use helix_view::graphics::{Rect, Color, Modifier, Style}; +/// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style}; /// # use helix_tui::buffer::Buffer; /// let styles = [ /// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), @@ -430,7 +453,8 @@ impl FromStr for Modifier { /// Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Reset), -/// underline: Some(Color::Reset), +/// underline_color: Some(Color::Reset), +/// underline_style: Some(UnderlineStyle::Reset), /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -442,7 +466,8 @@ impl FromStr for Modifier { pub struct Style { pub fg: Option, pub bg: Option, - pub underline: Option, + pub underline_color: Option, + pub underline_style: Option, pub add_modifier: Modifier, pub sub_modifier: Modifier, } @@ -452,7 +477,8 @@ impl Default for Style { Style { fg: None, bg: None, - underline: None, + underline_color: None, + underline_style: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::empty(), } @@ -465,7 +491,8 @@ impl Style { Style { fg: Some(Color::Reset), bg: Some(Color::Reset), - underline: Some(Color::Reset), + underline_color: None, + underline_style: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::all(), } @@ -507,12 +534,27 @@ impl Style { /// /// ```rust /// # use helix_view::graphics::{Color, Style}; - /// let style = Style::default().underline(Color::Blue); - /// let diff = Style::default().underline(Color::Red); - /// assert_eq!(style.patch(diff), Style::default().underline(Color::Red)); + /// let style = Style::default().underline_color(Color::Blue); + /// let diff = Style::default().underline_color(Color::Red); + /// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red)); + /// ``` + pub fn underline_color(mut self, color: Color) -> Style { + self.underline_color = Some(color); + self + } + + /// Changes the underline style. + /// + /// ## Examples + /// + /// ```rust + /// # use helix_view::graphics::{UnderlineStyle, Style}; + /// let style = Style::default().underline_style(UnderlineStyle::Line); + /// let diff = Style::default().underline_style(UnderlineStyle::Curl); + /// assert_eq!(style.patch(diff), Style::default().underline_style(UnderlineStyle::Curl)); /// ``` - pub fn underline(mut self, color: Color) -> Style { - self.underline = Some(color); + pub fn underline_style(mut self, style: UnderlineStyle) -> Style { + self.underline_style = Some(style); self } @@ -572,7 +614,8 @@ impl Style { pub fn patch(mut self, other: Style) -> Style { self.fg = other.fg.or(self.fg); self.bg = other.bg.or(self.bg); - self.underline = other.underline.or(self.underline); + self.underline_color = other.underline_color.or(self.underline_color); + self.underline_style = other.underline_style.or(self.underline_style); self.add_modifier.remove(other.sub_modifier); self.add_modifier.insert(other.add_modifier); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index ab0e298661775..2c207d2768d4d 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,7 +1,7 @@ use std::fmt::Write; use crate::{ - graphics::{Color, Modifier, Style}, + graphics::{Color, Style, UnderlineStyle}, Document, Editor, Theme, View, }; @@ -147,7 +147,7 @@ pub fn breakpoints<'doc>( .find(|breakpoint| breakpoint.line == line)?; let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { - error.add_modifier(Modifier::UNDERLINED) + error.underline_style(UnderlineStyle::Line) } else if breakpoint.condition.is_some() { error } else if breakpoint.log_message.is_some() { diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 5ce1b2c579dfc..90185937c94ae 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -10,6 +10,7 @@ use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; use toml::Value; +use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME: Lazy = Lazy::new(|| { @@ -263,20 +264,38 @@ impl ThemePalette { .ok_or(format!("Theme: invalid modifier: {}", value)) } + pub fn parse_underline_style(value: &Value) -> Result { + value + .as_str() + .and_then(|s| s.parse().ok()) + .ok_or(format!("Theme: invalid underline_style: {}", value)) + } + pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> { if let Value::Table(entries) = value { for (name, value) in entries { match name.as_str() { "fg" => *style = style.fg(self.parse_color(value)?), "bg" => *style = style.bg(self.parse_color(value)?), - "underline" => *style = style.underline(self.parse_color(value)?), + "underline_color" => *style = style.underline_color(self.parse_color(value)?), + "underline_style" => { + warn!("found style"); + *style = style.underline_style(Self::parse_underline_style(&value)?) + } "modifiers" => { let modifiers = value .as_array() .ok_or("Theme: modifiers should be an array")?; for modifier in modifiers { - *style = style.add_modifier(Self::parse_modifier(modifier)?); + if modifier + .as_str() + .map_or(false, |modifier| modifier == "underlined") + { + *style = style.underline_style(UnderlineStyle::Line); + } else { + *style = style.add_modifier(Self::parse_modifier(modifier)?); + } } } _ => return Err(format!("Theme: invalid style attribute: {}", name)), diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index d1a5756efd39a..fa6b34ab49858 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -92,8 +92,8 @@ "info" = { fg = "light_blue" } "hint" = { fg = "light_gray3" } -"diagnostic.error" = {underline = "red", modifiers = ["undercurled"] } -"diagnostic" = {underline = "gold", modifiers = ["undercurled"] } +"diagnostic.error" = {underline_color = "red", underline_style = "curl"} +"diagnostic" = {underline_color = "gold", underline_style = "curl" } [palette] white = "#ffffff" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index e2bc2c472d160..5f337a8d5d1f7 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -39,10 +39,10 @@ "diff.delta" = "gold" "diff.minus" = "red" -"diagnostic.info" = { underline = "blue", modifiers = ["undercurled"] } -"diagnostic.hint" = { underline = "green", modifiers = ["undercurled"] } -"diagnostic.warning" = { underline = "yellow", modifiers = ["undercurled"] } -"diagnostic.error" = { underline = "red", modifiers = ["undercurled"] } +"diagnostic.info" = { underline_color = "blue", underline_style = "curl" } +"diagnostic.hint" = { underline_color = "green", underline_style = "curl" } +"diagnostic.warning" = { underline_color = "yellow", underline_style = "curl" } +"diagnostic.error" = { underline_color = "red", underline_style = "curl" } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } From 114610f7dc5d6395ef5cce9111a363f7c8d879a4 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 6 Oct 2022 20:46:24 +0200 Subject: [PATCH 007/151] switch to termini for terminfo --- Cargo.lock | 17 ++++++++++------- helix-tui/Cargo.toml | 2 +- helix-tui/src/backend/crossterm.rs | 6 +++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f980c4175a0b4..03b3a6d4f4fcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,12 +176,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cxterminfo" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da92c5e3aaf2cc1fea346d9b3bac0c59c6ffc1d1d46f18d991d449912a3e6f07" - [[package]] name = "dirs-next" version = "2.0.0" @@ -510,10 +504,10 @@ dependencies = [ "bitflags", "cassowary", "crossterm", - "cxterminfo", "helix-core", "helix-view", "serde", + "termini", "unicode-segmentation", ] @@ -1105,6 +1099,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termini" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00733d628ac0a8bd4fd3171a28eb6c09759ae1b43d8b587eadebaccee01d01a3" +dependencies = [ + "dirs-next", +] + [[package]] name = "textwrap" version = "0.15.1" diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 1c6a6a8d45b2c..a4a1c389f34db 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -20,7 +20,7 @@ bitflags = "1.3" cassowary = "0.3" unicode-segmentation = "1.10" crossterm = { version = "0.25", optional = true } -cxterminfo = "0.2" +termini = "0.1" serde = { version = "1", "optional" = true, features = ["derive"]} helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-core = { version = "0.6", path = "../helix-core" } diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 3e6dc5f59f4b6..4d8c665030e13 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -27,13 +27,13 @@ impl Capabilities { /// on the $TERM environment variable. If detection fails, returns /// a default value where no capability is supported. pub fn from_env_or_default() -> Self { - match cxterminfo::terminfo::TermInfo::from_env() { + match termini::TermInfo::from_env() { Err(_) => Capabilities::default(), Ok(t) => Capabilities { // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284 // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines - has_extended_underlines: t.get_ext_string("Smulx").is_some() - || *t.get_ext_bool("Su").unwrap_or(&false) + has_extended_underlines: t.extended_cap("Smulx").is_some() + || t.extended_cap("Su").is_some() || vte_version() >= Some(5102), }, } From 7bc324fde986fab2ded2ad29d7b5244521eddc44 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 6 Oct 2022 20:50:54 +0200 Subject: [PATCH 008/151] make casing consistent with other configuration --- book/src/themes.md | 6 +++--- helix-view/src/theme.rs | 4 ++-- runtime/themes/dark_plus.toml | 4 ++-- runtime/themes/onedark.toml | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 450b3364958fa..b386973a72ffd 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -13,7 +13,7 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix Each line in the theme file is specified as below: ```toml -key = { fg = "#ffffff", bg = "#000000", underline_color = "#ff0000", underline_style = "curl", modifiers = ["bold", "italic"] } +key = { fg = "#ffffff", bg = "#000000", underline-color = "#ff0000", underline-style = "curl", modifiers = ["bold", "italic"] } ``` where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline_style` the underline style, `underline_color` the underline color (only meaningful if an underline style is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. @@ -91,7 +91,7 @@ Less common modifiers might not be supported by your terminal emulator. ### Underline Style -One of the following values may be used as an `underline_styles`. +One of the following values may be used as an `underline-style`. Some styles might not be supported by your terminal emulator. @@ -101,7 +101,7 @@ Some styles might not be supported by your terminal emulator. | `curl` | | `dashed` | | `dot` | -| `double-line` | +| `double_line` | ### Scopes diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 90185937c94ae..b1c96f94f0e26 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -277,8 +277,8 @@ impl ThemePalette { match name.as_str() { "fg" => *style = style.fg(self.parse_color(value)?), "bg" => *style = style.bg(self.parse_color(value)?), - "underline_color" => *style = style.underline_color(self.parse_color(value)?), - "underline_style" => { + "underline-color" => *style = style.underline_color(self.parse_color(value)?), + "underline-style" => { warn!("found style"); *style = style.underline_style(Self::parse_underline_style(&value)?) } diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index fa6b34ab49858..afbd1b7140ffe 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -92,8 +92,8 @@ "info" = { fg = "light_blue" } "hint" = { fg = "light_gray3" } -"diagnostic.error" = {underline_color = "red", underline_style = "curl"} -"diagnostic" = {underline_color = "gold", underline_style = "curl" } +"diagnostic.error" = {underline-color = "red", underline-style = "curl"} +"diagnostic" = {underline-color = "gold", underline-style = "curl" } [palette] white = "#ffffff" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 5f337a8d5d1f7..cce0474f4dc0d 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -39,10 +39,10 @@ "diff.delta" = "gold" "diff.minus" = "red" -"diagnostic.info" = { underline_color = "blue", underline_style = "curl" } -"diagnostic.hint" = { underline_color = "green", underline_style = "curl" } -"diagnostic.warning" = { underline_color = "yellow", underline_style = "curl" } -"diagnostic.error" = { underline_color = "red", underline_style = "curl" } +"diagnostic.info" = { underline-color = "blue", underline-style = "curl" } +"diagnostic.hint" = { underline-color = "green", underline-style = "curl" } +"diagnostic.warning" = { underline-color = "yellow", underline-style = "curl" } +"diagnostic.error" = { underline-color = "red", underline-style = "curl" } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } From 4c36c067b06e3bba24e0b2bd0f40f259ca6c8b41 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 7 Oct 2022 00:09:55 +0200 Subject: [PATCH 009/151] avoid visual artificats on terminal emulators that do not support underline colors --- helix-tui/src/backend/crossterm.rs | 77 ++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 4d8c665030e13..7b3b0817e52a3 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -4,13 +4,16 @@ use crossterm::{ execute, queue, style::{ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, - SetForegroundColor, SetUnderlineColor, + SetForegroundColor, }, terminal::{self, Clear, ClearType}, + Command, }; use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle}; -use std::io::{self, Write}; - +use std::{ + fmt, + io::{self, Write}, +}; fn vte_version() -> Option { std::env::var("VTE_VERSION").ok()?.parse().ok() } @@ -108,18 +111,19 @@ where map_error(queue!(self.buffer, SetBackgroundColor(color)))?; bg = cell.bg; } - if cell.underline_color != underline_color { - let color = CColor::from(cell.underline_color); - map_error(queue!(self.buffer, SetUnderlineColor(color)))?; - underline_color = cell.underline_color; - } let mut new_underline_style = cell.underline_style; if !self.capabilities.has_extended_underlines { match new_underline_style { - UnderlineStyle::Reset => (), + UnderlineStyle::Reset | UnderlineStyle::Line => (), _ => new_underline_style = UnderlineStyle::Line, } + + if cell.underline_color != underline_color { + let color = CColor::from(cell.underline_color); + map_error(queue!(self.buffer, SetUnderlineColor(color)))?; + underline_color = cell.underline_color; + } } if new_underline_style != underline_style { @@ -244,3 +248,58 @@ impl ModifierDiff { Ok(()) } } + +/// Crossterm uses semicolon as a seperator for colors +/// this is actually not spec compliant (altough commonly supported) +/// However the correct approach is to use colons as a seperator. +/// This usually doesn't make a difference for emulators that do support colored underlines. +/// However terminals that do not support colored underlines will ignore underlines colors with colons +/// while escape sequences with semicolons are always processed which leads to weird visual artifacts. +/// See [this nvim issue](https://github.com/neovim/neovim/issues/9270) for details +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetUnderlineColor(pub CColor); + +impl Command for SetUnderlineColor { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + let color = self.0; + + if color == CColor::Reset { + write!(f, "\x1b[59m")?; + return Ok(()); + } + f.write_str("\x1b[58:")?; + + let res = match color { + CColor::Black => f.write_str("5:0"), + CColor::DarkGrey => f.write_str("5:8"), + CColor::Red => f.write_str("5:9"), + CColor::DarkRed => f.write_str("5:1"), + CColor::Green => f.write_str("5:10"), + CColor::DarkGreen => f.write_str("5:2"), + CColor::Yellow => f.write_str("5:11"), + CColor::DarkYellow => f.write_str("5:3"), + CColor::Blue => f.write_str("5:12"), + CColor::DarkBlue => f.write_str("5:4"), + CColor::Magenta => f.write_str("5:13"), + CColor::DarkMagenta => f.write_str("5:5"), + CColor::Cyan => f.write_str("5:14"), + CColor::DarkCyan => f.write_str("5:6"), + CColor::White => f.write_str("5:15"), + CColor::Grey => f.write_str("5:7"), + CColor::Rgb { r, g, b } => write!(f, "2::{}:{}:{}", r, g, b), + CColor::AnsiValue(val) => write!(f, "5:{}", val), + _ => Ok(()), + }; + res?; + write!(f, "m")?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> crossterm::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "SetUnderlineColor not supported by winapi.", + )) + } +} From ad0eb4094b39e458e9681eaa83539ea6cf09e5de Mon Sep 17 00:00:00 2001 From: pascalkuthe Date: Sat, 8 Oct 2022 16:39:02 +0200 Subject: [PATCH 010/151] add deprectation not for underlined modifier to docs --- book/src/themes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/book/src/themes.md b/book/src/themes.md index b386973a72ffd..c5a623b07101b 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -89,6 +89,9 @@ Less common modifiers might not be supported by your terminal emulator. | `hidden` | | `crossed_out` | +> Note: The `underlined` modifier is deprecated and only available for backwards compatability. +> It's behaviour is equivalent to `underline-style="line"` + ### Underline Style One of the following values may be used as an `underline-style`. From 2f7088c1f37f11606944a9db52814a652f97fdcd Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sat, 8 Oct 2022 17:43:32 +0200 Subject: [PATCH 011/151] fix typo Co-authored-by: Omnikar --- book/src/themes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/themes.md b/book/src/themes.md index c5a623b07101b..e84f870598387 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -90,7 +90,7 @@ Less common modifiers might not be supported by your terminal emulator. | `crossed_out` | > Note: The `underlined` modifier is deprecated and only available for backwards compatability. -> It's behaviour is equivalent to `underline-style="line"` +> Its behaviour is equivalent to `underline-style="line"`. ### Underline Style From 328c4d002f5eaef2b79c73a48ce5011e89425b54 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 11 Oct 2022 12:11:33 +0200 Subject: [PATCH 012/151] adress review comments --- book/src/themes.md | 2 +- helix-view/src/graphics.rs | 18 +++++++++--------- helix-view/src/theme.rs | 3 +-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index e84f870598387..66ad380e6c801 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -16,7 +16,7 @@ Each line in the theme file is specified as below: key = { fg = "#ffffff", bg = "#000000", underline-color = "#ff0000", underline-style = "curl", modifiers = ["bold", "italic"] } ``` -where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline_style` the underline style, `underline_color` the underline color (only meaningful if an underline style is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. +where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline-style` the underline style, `underline-color` the underline color (only meaningful if an underline style is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. To specify only the foreground color: diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 01344748fd096..4374a5371442c 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -365,16 +365,16 @@ bitflags! { /// /// let m = Modifier::BOLD | Modifier::ITALIC; /// ``` - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case"))] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Modifier: u16 { - const BOLD = 0b0000_0000_0000_0001; - const DIM = 0b0000_0000_0000_0010; - const ITALIC = 0b0000_0000_0000_0100; - const SLOW_BLINK = 0b0000_0000_0001_0000; - const RAPID_BLINK = 0b0000_0000_0010_0000; - const REVERSED = 0b0000_0000_0100_0000; - const HIDDEN = 0b0000_0000_1000_0000; - const CROSSED_OUT = 0b0000_0001_0000_0000; + const BOLD = 0b0000_0000_0001; + const DIM = 0b0000_0000_0010; + const ITALIC = 0b0000_0000_0100; + const SLOW_BLINK = 0b0000_0001_0000; + const RAPID_BLINK = 0b0000_0010_0000; + const REVERSED = 0b0000_0100_0000; + const HIDDEN = 0b0000_1000_0000; + const CROSSED_OUT = 0b0001_0000_0000; } } diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index b1c96f94f0e26..c32b3edf0148c 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -268,7 +268,7 @@ impl ThemePalette { value .as_str() .and_then(|s| s.parse().ok()) - .ok_or(format!("Theme: invalid underline_style: {}", value)) + .ok_or(format!("Theme: invalid underline-style: {}", value)) } pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> { @@ -279,7 +279,6 @@ impl ThemePalette { "bg" => *style = style.bg(self.parse_color(value)?), "underline-color" => *style = style.underline_color(self.parse_color(value)?), "underline-style" => { - warn!("found style"); *style = style.underline_style(Self::parse_underline_style(&value)?) } "modifiers" => { From 963a0ac0bbc43e2b26a9662bc67dd226582ad12a Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 12 Oct 2022 11:33:48 +0200 Subject: [PATCH 013/151] fix terminfo detection --- Cargo.lock | 4 ++-- helix-tui/src/backend/crossterm.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03b3a6d4f4fcb..6af26d378e3e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1101,9 +1101,9 @@ dependencies = [ [[package]] name = "termini" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00733d628ac0a8bd4fd3171a28eb6c09759ae1b43d8b587eadebaccee01d01a3" +checksum = "394766021ef3dae8077f080518cdf5360831990f77f5708d5e3594c9b3efa2f9" dependencies = [ "dirs-next", ] diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 7b3b0817e52a3..7c7250fa4fd3f 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -113,21 +113,21 @@ where } let mut new_underline_style = cell.underline_style; - if !self.capabilities.has_extended_underlines { - match new_underline_style { - UnderlineStyle::Reset | UnderlineStyle::Line => (), - _ => new_underline_style = UnderlineStyle::Line, - } - + if self.capabilities.has_extended_underlines { if cell.underline_color != underline_color { let color = CColor::from(cell.underline_color); map_error(queue!(self.buffer, SetUnderlineColor(color)))?; underline_color = cell.underline_color; } + } else { + match new_underline_style { + UnderlineStyle::Reset | UnderlineStyle::Line => (), + _ => new_underline_style = UnderlineStyle::Line, + } } if new_underline_style != underline_style { - let attr = CAttribute::from(cell.underline_style); + let attr = CAttribute::from(new_underline_style); map_error(queue!(self.buffer, SetAttribute(attr)))?; underline_style = new_underline_style; } From 66a49080bc7e492c37f9cd10ed36a696de1787a3 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 13 Oct 2022 19:03:58 +0200 Subject: [PATCH 014/151] merge underline-style and underline-color into a single table --- book/src/themes.md | 10 +++++----- helix-view/src/theme.rs | 21 ++++++++++++++++----- runtime/themes/dark_plus.toml | 4 ++-- runtime/themes/onedark.toml | 8 ++++---- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 66ad380e6c801..9738912c705fe 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix Each line in the theme file is specified as below: ```toml -key = { fg = "#ffffff", bg = "#000000", underline-color = "#ff0000", underline-style = "curl", modifiers = ["bold", "italic"] } +key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] } ``` -where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline-style` the underline style, `underline-color` the underline color (only meaningful if an underline style is enabled), and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. +where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. To specify only the foreground color: @@ -89,12 +89,12 @@ Less common modifiers might not be supported by your terminal emulator. | `hidden` | | `crossed_out` | -> Note: The `underlined` modifier is deprecated and only available for backwards compatability. -> Its behaviour is equivalent to `underline-style="line"`. +> Note: The `underlined` modifier is deprecated and only available for backwards compatibility. +> Its behavior is equivalent to setting `underline.style="line"`. ### Underline Style -One of the following values may be used as an `underline-style`. +One of the following values may be used as a value for `underline.style`. Some styles might not be supported by your terminal emulator. diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index c32b3edf0148c..aaef28b22c28c 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -268,18 +268,29 @@ impl ThemePalette { value .as_str() .and_then(|s| s.parse().ok()) - .ok_or(format!("Theme: invalid underline-style: {}", value)) + .ok_or(format!("Theme: invalid underline style: {}", value)) } pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> { if let Value::Table(entries) = value { - for (name, value) in entries { + for (name, mut value) in entries { match name.as_str() { "fg" => *style = style.fg(self.parse_color(value)?), "bg" => *style = style.bg(self.parse_color(value)?), - "underline-color" => *style = style.underline_color(self.parse_color(value)?), - "underline-style" => { - *style = style.underline_style(Self::parse_underline_style(&value)?) + "underline" => { + let table = value + .as_table_mut() + .ok_or("Theme: underline must be table")?; + if let Some(value) = table.remove("color") { + *style = style.underline_color(self.parse_color(value)?); + } + if let Some(value) = table.remove("style") { + *style = style.underline_style(Self::parse_underline_style(&value)?); + } + + if let Some(attr) = table.keys().next() { + return Err(format!("Theme: invalid underline attribute: {attr}")); + } } "modifiers" => { let modifiers = value diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index afbd1b7140ffe..fbb58e646029b 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -92,8 +92,8 @@ "info" = { fg = "light_blue" } "hint" = { fg = "light_gray3" } -"diagnostic.error" = {underline-color = "red", underline-style = "curl"} -"diagnostic" = {underline-color = "gold", underline-style = "curl" } +"diagnostic.error".underline = { color = "red", style = "curl" } +"diagnostic".underline = { color = "gold", style = "curl" } [palette] white = "#ffffff" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index cce0474f4dc0d..c4a56b90f4a99 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -39,10 +39,10 @@ "diff.delta" = "gold" "diff.minus" = "red" -"diagnostic.info" = { underline-color = "blue", underline-style = "curl" } -"diagnostic.hint" = { underline-color = "green", underline-style = "curl" } -"diagnostic.warning" = { underline-color = "yellow", underline-style = "curl" } -"diagnostic.error" = { underline-color = "red", underline-style = "curl" } +"diagnostic.info".underline = { color = "blue", style = "curl" } +"diagnostic.hint".underline = { color = "green", style = "curl" } +"diagnostic.warning".underline = { color = "yellow", style = "curl" } +"diagnostic.error".underline = { color = "red", style = "curl" } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] } From 63fe423710df3198293ea14a357b2884ae0445b3 Mon Sep 17 00:00:00 2001 From: Ben White-Horne <32932209+kneasle@users.noreply.github.com> Date: Sun, 16 Oct 2022 17:58:18 +0100 Subject: [PATCH 015/151] Show keys required to enter each minor mode (#4302) Co-authored-by: Michael Davis --- book/src/keymap.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 7fd146a6928c5..0882381e67bf9 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -167,10 +167,13 @@ These sub-modes are accessible from normal mode and typically switch back to nor #### View mode +Accessed by typing `z` in [normal mode](#normal-mode). + View mode is intended for scrolling and manipulating the view without changing -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). +the selection. The "sticky" variant of this mode (accessed by typing `Z` in +normal 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 | @@ -188,6 +191,8 @@ over text and not actively editing it). #### Goto mode +Accessed by typing `g` in [normal mode](#normal-mode). + Jumps to various locations. | Key | Description | Command | @@ -213,9 +218,10 @@ Jumps to various locations. #### Match mode -Enter this mode using `m` from normal mode. See the relevant section -in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround) -and [textobject](./usage.md#textobject) usage. +Accessed by typing `m` in [normal mode](#normal-mode). + +See the relevant section in [Usage](./usage.md) for an explanation about +[surround](./usage.md#surround) and [textobject](./usage.md#textobject) usage. | Key | Description | Command | | ----- | ----------- | ------- | @@ -230,6 +236,8 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`). #### Window mode +Accessed by typing `Ctrl-w` in [normal mode](#normal-mode). + This layer is similar to Vim keybindings as Kakoune does not support window. | Key | Description | Command | @@ -252,8 +260,9 @@ This layer is similar to Vim keybindings as Kakoune does not support window. #### Space mode -This layer is a kludge of mappings, mostly pickers. +Accessed by typing `Space` in [normal mode](#normal-mode). +This layer is a kludge of mappings, mostly pickers. | Key | Description | Command | | ----- | ----------- | ------- | From 5f4f171b73232e5ec5e4b7153084a93033ab5cf0 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 16 Oct 2022 14:52:04 -0500 Subject: [PATCH 016/151] Fix debug assertion for diagnostic sort order (#4319) The debug assertion that document diagnostics are sorted incorrectly panics for cases like `[161..164, 162..162]`. The merging behavior in the following lines that relies on the assertion only needs the input ranges to be sorted by `range.start`, so this change simplifies the assertion to only catch violations of that assumption. --- helix-term/src/ui/editor.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index fb70fa0f7eb65..8e9d863118ed8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -302,16 +302,7 @@ impl EditorView { let mut warning_vec = Vec::new(); let mut error_vec = Vec::new(); - let diagnostics = doc.diagnostics(); - - // Diagnostics must be sorted by range. Otherwise, the merge strategy - // below would not be accurate. - debug_assert!(diagnostics - .windows(2) - .all(|window| window[0].range.start <= window[1].range.start - && window[0].range.end <= window[1].range.end)); - - for diagnostic in diagnostics { + for diagnostic in doc.diagnostics() { // Separate diagnostics into different Vecs by severity. let (vec, scope) = match diagnostic.severity { Some(Severity::Info) => (&mut info_vec, info), @@ -325,6 +316,11 @@ impl EditorView { // merge the two together. Otherwise push a new span. match vec.last_mut() { Some((_, range)) if diagnostic.range.start <= range.end => { + // This branch merges overlapping diagnostics, assuming that the current + // diagnostic starts on range.start or later. If this assertion fails, + // we will discard some part of `diagnostic`. This implies that + // `doc.diagnostics()` is not sorted by `diagnostic.range`. + debug_assert!(range.start <= diagnostic.range.start); range.end = diagnostic.range.end.max(range.end) } _ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)), From 50b191a7dff4a6c311437d3e5fcd1ce36b300643 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 16 Oct 2022 14:59:57 -0500 Subject: [PATCH 017/151] Log failures to load tree-sitter parsers as error (#4315) Info logs don't show up in the log file by default, but this line should: failures to load tree-sitter parser objects are useful errors. A parser might fail to load it is misconfigured (https://github.com/helix-editor/helix/pull/4303#discussion_r996448543) or if the file does not exist. --- helix-core/src/syntax.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 8c51d5ebb049d..61d382fdec6e3 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -368,7 +368,13 @@ impl LanguageConfiguration { None } else { let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id)) - .map_err(|e| log::info!("{}", e)) + .map_err(|err| { + log::error!( + "Failed to load tree-sitter parser for language {:?}: {}", + self.language_id, + err + ) + }) .ok()?; let config = HighlightConfiguration::new( language, From a330b5d224274d3ca01def99408ce9e614a97c94 Mon Sep 17 00:00:00 2001 From: Brandon Dong Date: Sun, 16 Oct 2022 17:19:19 -0700 Subject: [PATCH 018/151] Fix selecting pop dark theme (#4323) --- runtime/themes/pop-dark.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/themes/pop-dark.toml b/runtime/themes/pop-dark.toml index bac0d0f98f52c..6ad0111b7520b 100644 --- a/runtime/themes/pop-dark.toml +++ b/runtime/themes/pop-dark.toml @@ -43,7 +43,6 @@ module = { bg = 'orangeL' } special = { fg = 'orangeW' } operator = { fg = 'orangeY' } attribute = { fg = 'orangeL' } -attribute = { fg = 'orangeL' } namespace = { fg = 'orangeL' } 'type' = { fg = 'redH' } 'type.builtin' = { fg = 'orangeL' } From b29531ceab18625b3994f48937cf7bbef8f3ab89 Mon Sep 17 00:00:00 2001 From: echoriiku <42903366+echoriiku@users.noreply.github.com> Date: Sun, 16 Oct 2022 22:34:38 -0300 Subject: [PATCH 019/151] Fix readability of highlighted text on the rose_pine themes using official palettes (#4221) --- runtime/themes/rose_pine.toml | 6 +++--- runtime/themes/rose_pine_dawn.toml | 6 +++--- runtime/themes/rose_pine_moon.toml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index 06be30838c908..14e240dd505e1 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -79,6 +79,6 @@ rose = "#ebbcba" pine = "#31748f" foam = "#9ccfd8" iris = "#c4a7e7" -highlight = "#2a2837" -highlightInactive = "#211f2d" -highlightOverlay = "#3a384a" +highlight = "#403d52" +highlightInactive = "#21202e" +highlightOverlay = "#524f67" diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml index 129ae9c76bc07..c6609c218b6c4 100644 --- a/runtime/themes/rose_pine_dawn.toml +++ b/runtime/themes/rose_pine_dawn.toml @@ -76,6 +76,6 @@ rose = "#d7827e" pine = "#286983" foam = "#56949f" iris = "#907aa9" -highlight = "#eee9e6" -highlightInactive = "#f2ede9" -highlightOverlay = "#e4dfde" +highlight = "#dfdad9" +highlightInactive = "#f4ede8" +highlightOverlay = "#cecacd" diff --git a/runtime/themes/rose_pine_moon.toml b/runtime/themes/rose_pine_moon.toml index 4706bb17a4e2b..fc27d2c0a809f 100644 --- a/runtime/themes/rose_pine_moon.toml +++ b/runtime/themes/rose_pine_moon.toml @@ -83,6 +83,6 @@ rose = "#ea9a97" pine = "#3e8fb0" foam = "#9ccfd8" iris = "#c4a7e7" -highlight = "#2a283e" -highlightInactive = "#44415a" +highlight = "#44415a" +highlightInactive = "#2a283e" highlightOverlay = "#56526e" From 17488f14d6d28361d3ae060492e2f31fde00f00c Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Fri, 14 Oct 2022 03:45:18 +0000 Subject: [PATCH 020/151] Fix Cairo comment token --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 08d02a49ef009..da7b10bd0222d 100644 --- a/languages.toml +++ b/languages.toml @@ -1440,7 +1440,7 @@ scope = "source.cairo" injection-regex = "cairo" file-types = ["cairo"] roots = [] -comment-token = "#" +comment-token = "//" indent = { tab-width = 4, unit = " " } [[grammar]] From 431f9c14f35868c6825a3e7e858148d1f2bcddbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 14 Oct 2022 16:25:33 +0900 Subject: [PATCH 021/151] Support Cairo 0.10 syntax --- languages.toml | 2 +- runtime/queries/cairo/highlights.scm | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/languages.toml b/languages.toml index da7b10bd0222d..d352853382635 100644 --- a/languages.toml +++ b/languages.toml @@ -1445,7 +1445,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "cairo" -source = { git = "https://github.com/archseer/tree-sitter-cairo", rev = "5155c6eb40db6d437f4fa41b8bcd8890a1c91716" } +source = { git = "https://github.com/archseer/tree-sitter-cairo", rev = "e2f9b3d75f483fcfda03cf07e4176869357bfb52" } [[language]] name = "cpon" diff --git a/runtime/queries/cairo/highlights.scm b/runtime/queries/cairo/highlights.scm index 3a30d188ed6a0..c10ce5e074fbf 100644 --- a/runtime/queries/cairo/highlights.scm +++ b/runtime/queries/cairo/highlights.scm @@ -36,7 +36,6 @@ [ "if" "else" - "end" "assert" "with" "with_attr" @@ -54,7 +53,6 @@ "const" "local" "struct" - "member" "alloc_locals" "tempvar" ] @keyword From 09a6df199e535270f15901cdaec99cfffa0f4043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 14 Oct 2022 16:25:33 +0900 Subject: [PATCH 022/151] Support Cairo 0.10 syntax --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index d352853382635..7fa3e2119c683 100644 --- a/languages.toml +++ b/languages.toml @@ -1445,7 +1445,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "cairo" -source = { git = "https://github.com/archseer/tree-sitter-cairo", rev = "e2f9b3d75f483fcfda03cf07e4176869357bfb52" } +source = { git = "https://github.com/archseer/tree-sitter-cairo", rev = "b249662a1eefeb4d71c9529cdd971e74fecc10fe" } [[language]] name = "cpon" From 4e691d62470a4f92ee5bf2e2609a97d0e1ec832c Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Mon, 17 Oct 2022 13:15:42 +0530 Subject: [PATCH 023/151] Change diagnostic picker keybind to d (#4229) Also changes workspace diagnostic picker bindings to D and changes the debug menu keybind to g, the previous diagnostic picker keybind. This brings the diagnostic picker bindings more in line with the jump to next/previous diagnostic bindings which are currently on ]d and [d. --- book/src/keymap.md | 4 ++-- helix-term/src/keymap/default.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 0882381e67bf9..5bdbdd2935b4a 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -273,8 +273,8 @@ This layer is a kludge of mappings, mostly pickers. | `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | | `s` | Open document symbol picker (**LSP**) | `symbol_picker` | | `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | -| `g` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | -| `G` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` +| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | +| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | | `r` | Rename symbol (**LSP**) | `rename_symbol` | | `a` | Apply code action (**LSP**) | `code_action` | | `'` | Open last fuzzy picker | `last_picker` | diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 6c327ee6bc47e..118764d975858 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -209,11 +209,11 @@ pub fn default() -> HashMap { "j" => jumplist_picker, "s" => symbol_picker, "S" => workspace_symbol_picker, - "g" => diagnostics_picker, - "G" => workspace_diagnostics_picker, + "d" => diagnostics_picker, + "D" => workspace_diagnostics_picker, "a" => code_action, "'" => last_picker, - "d" => { "Debug (experimental)" sticky=true + "g" => { "Debug (experimental)" sticky=true "l" => dap_launch, "b" => dap_toggle_breakpoint, "c" => dap_continue, From 3ba665d804c8d88bc3d59a52296f349acdb6d436 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Mon, 10 Oct 2022 11:13:25 +0300 Subject: [PATCH 024/151] Fix rendering of lines longer than 2^16 Before things would be cast to u16 earlier than needed, which would cause problems for insanely long lines (longer than 2^16 ~ 65 thousand) --- helix-term/src/ui/editor.rs | 19 +++++++++---------- helix-view/src/editor.rs | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 8e9d863118ed8..c6a835467a6cc 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -428,7 +428,7 @@ impl EditorView { let characters = &whitespace.characters; let mut spans = Vec::new(); - let mut visual_x = 0u16; + let mut visual_x = 0usize; let mut line = 0u16; let tab_width = doc.tab_width(); let tab = if whitespace.render.tab() == WhitespaceRenderValue::All { @@ -465,14 +465,13 @@ impl EditorView { return; } - let starting_indent = - (offset.col / tab_width) as u16 + config.indent_guides.skip_levels; + let starting_indent = (offset.col / tab_width) + config.indent_guides.skip_levels; // TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some // extra loops if the code is deeply nested. - for i in starting_indent..(indent_level / tab_width as u16) { + for i in starting_indent..(indent_level / tab_width) { surface.set_string( - viewport.x + (i * tab_width as u16) - offset.col as u16, + (viewport.x as usize + (i * tab_width) - offset.col) as u16, viewport.y + line, &indent_guide_char, indent_guide_style, @@ -518,14 +517,14 @@ impl EditorView { use helix_core::graphemes::{grapheme_width, RopeGraphemes}; for grapheme in RopeGraphemes::new(text) { - let out_of_bounds = visual_x < offset.col as u16 - || visual_x >= viewport.width + offset.col as u16; + let out_of_bounds = offset.col > (visual_x as usize) + || (visual_x as usize) >= viewport.width as usize + offset.col; if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { // we still want to render an empty cell with the style surface.set_string( - viewport.x + visual_x - offset.col as u16, + (viewport.x as usize + visual_x - offset.col) as u16, viewport.y + line, &newline, style.patch(whitespace_style), @@ -573,7 +572,7 @@ impl EditorView { if !out_of_bounds { // if we're offscreen just keep going until we hit a new line surface.set_string( - viewport.x + visual_x - offset.col as u16, + (viewport.x as usize + visual_x - offset.col) as u16, viewport.y + line, display_grapheme, if is_whitespace { @@ -606,7 +605,7 @@ impl EditorView { last_line_indent_level = visual_x; } - visual_x = visual_x.saturating_add(width as u16); + visual_x = visual_x.saturating_add(width); } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 60b3880c6e14b..dd1e30f95b7e6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -558,7 +558,7 @@ impl Default for WhitespaceCharacters { pub struct IndentGuidesConfig { pub render: bool, pub character: char, - pub skip_levels: u16, + pub skip_levels: usize, } impl Default for IndentGuidesConfig { From 2c36e33e0ac55d01baacb9714d9d77d7dd532d98 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Tue, 11 Oct 2022 10:00:41 +0300 Subject: [PATCH 025/151] Make skip_levels a u8 --- helix-term/src/ui/editor.rs | 3 ++- helix-view/src/editor.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index c6a835467a6cc..e05136d02636b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -465,7 +465,8 @@ impl EditorView { return; } - let starting_indent = (offset.col / tab_width) + config.indent_guides.skip_levels; + let starting_indent = + (offset.col / tab_width) + config.indent_guides.skip_levels as usize; // TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some // extra loops if the code is deeply nested. diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index dd1e30f95b7e6..e9a3c63970176 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -558,7 +558,7 @@ impl Default for WhitespaceCharacters { pub struct IndentGuidesConfig { pub render: bool, pub character: char, - pub skip_levels: usize, + pub skip_levels: u8, } impl Default for IndentGuidesConfig { From 1de02a147c1738015aa26dea7c8fd313f3741d8e Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Tue, 11 Oct 2022 11:37:57 +0300 Subject: [PATCH 026/151] Only draw indent guides within bounds Better performance, and otherwise very long lines with lots of tabs will wrap around the u16 and come back on the other side, messing up the beginning skip_levels. --- helix-term/src/ui/editor.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index e05136d02636b..4074534bf3173 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -467,16 +467,14 @@ impl EditorView { let starting_indent = (offset.col / tab_width) + config.indent_guides.skip_levels as usize; - // TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some - // extra loops if the code is deeply nested. for i in starting_indent..(indent_level / tab_width) { - surface.set_string( - (viewport.x as usize + (i * tab_width) - offset.col) as u16, - viewport.y + line, - &indent_guide_char, - indent_guide_style, - ); + let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16; + let y = viewport.y + line; + if !surface.in_bounds(x, y) { + break; + } + surface.set_string(x, y, &indent_guide_char, indent_guide_style); } }; From 8bbddf90ffda7c37f8a87f71227a185007ddfae0 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Mon, 17 Oct 2022 14:23:50 +0300 Subject: [PATCH 027/151] Replace `in_bounds` with calculation of end_indent Instead of repeatedly checking if it is in_bounds, calculate the max_indent beforehand and just loop. I added a debug_assert to "prove" that it never tries drawing out of bounds. --- helix-term/src/ui/editor.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4074534bf3173..3cd2130ad8713 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -24,7 +24,7 @@ use helix_view::{ keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{borrow::Cow, path::PathBuf}; +use std::{borrow::Cow, cmp::min, path::PathBuf}; use tui::buffer::Buffer as Surface; @@ -468,12 +468,18 @@ impl EditorView { let starting_indent = (offset.col / tab_width) + config.indent_guides.skip_levels as usize; - for i in starting_indent..(indent_level / tab_width) { + // Don't draw indent guides outside of view + let end_indent = min( + indent_level, + // Add tab_width - 1 to round up, since the first visible + // indent might be a bit after offset.col + offset.col + viewport.width as usize + (tab_width - 1), + ) / tab_width; + + for i in starting_indent..end_indent { let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16; let y = viewport.y + line; - if !surface.in_bounds(x, y) { - break; - } + debug_assert!(surface.in_bounds(x, y)); surface.set_string(x, y, &indent_guide_char, indent_guide_style); } }; From b07ef6bec30e1c704a63579efa5cc433c22fcc19 Mon Sep 17 00:00:00 2001 From: Clay Date: Mon, 17 Oct 2022 09:05:14 -0700 Subject: [PATCH 028/151] Bump tree-sitter-elixir (#4333) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 7fa3e2119c683..3c324c78ab61d 100644 --- a/languages.toml +++ b/languages.toml @@ -107,7 +107,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "elixir" -source = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "1dabc1c790e07115175057863808085ea60dd08a" } +source = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "b20eaa75565243c50be5e35e253d8beb58f45d56" } [[language]] name = "fish" From 38746b04b44bc792ddca5d798f750366e478d258 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 19:55:15 -0500 Subject: [PATCH 029/151] build(deps): bump cachix/cachix-action from 10 to 11 (#4339) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cachix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 2d37b36a78d71..d4547e589aaba 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -17,7 +17,7 @@ jobs: uses: cachix/install-nix-action@v17 - name: Authenticate with Cachix - uses: cachix/cachix-action@v10 + uses: cachix/cachix-action@v11 with: name: helix authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} From 414214f883c73d341db3b2638eed5e077de8eb53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 19:56:29 -0500 Subject: [PATCH 030/151] build(deps): bump tokio-stream from 0.1.10 to 0.1.11 (#4341) 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 38ab8044e9687..c45d8739c89d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,9 +1197,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", "pin-project-lite", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 536a6ba6bcaff..ad432d96f064d 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tokio = { version = "1.21", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } -tokio-stream = "0.1.10" +tokio-stream = "0.1.11" which = "4.2" From fee5db161b67316f29254f29ce5eea3044ebfdac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 20:03:04 -0500 Subject: [PATCH 031/151] build(deps): bump cachix/install-nix-action from 17 to 18 (#4340) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cachix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index d4547e589aaba..bc72bb78df21d 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v3 - name: Install nix - uses: cachix/install-nix-action@v17 + uses: cachix/install-nix-action@v18 - name: Authenticate with Cachix uses: cachix/cachix-action@v11 From d17ffc47f008821e1b4ab04cd85d019ed8dc6fb9 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Tue, 18 Oct 2022 19:28:16 +0200 Subject: [PATCH 032/151] Use the same `WalkBuilder` configuration for the global search file picker as the default file picker (#4334) Skip searching .git in global search, similar to how file picker skips listing files in .git. --- helix-term/src/commands.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d5fc1ad934ebe..318dd035eea13 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1865,10 +1865,15 @@ fn global_search(cx: &mut Context) { .hidden(file_picker_config.hidden) .parents(file_picker_config.parents) .ignore(file_picker_config.ignore) + .follow_links(file_picker_config.follow_symlinks) .git_ignore(file_picker_config.git_ignore) .git_global(file_picker_config.git_global) .git_exclude(file_picker_config.git_exclude) .max_depth(file_picker_config.max_depth) + // We always want to ignore the .git directory, otherwise if + // `ignore` is turned off above, we end up with a lot of noise + // in our picker. + .filter_entry(|entry| entry.file_name() != ".git") .build_parallel() .run(|| { let mut searcher = searcher.clone(); From 1f74cf45d45804da8f33e173b26b3ddda82c706b Mon Sep 17 00:00:00 2001 From: Roberto Vidal Date: Tue, 18 Oct 2022 20:30:00 +0200 Subject: [PATCH 033/151] display tree sitter scopes in a popup (#4337) --- helix-term/src/commands/typed.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 8ab10da4cfd8d..1bfc81536d354 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1060,7 +1060,21 @@ fn tree_sitter_scopes( let pos = doc.selection(view.id).primary().cursor(text); let scopes = indent::get_scopes(doc.syntax(), text, pos); - cx.editor.set_status(format!("scopes: {:?}", &scopes)); + + let contents = format!("```json\n{:?}\n````", scopes); + + let callback = async move { + let call: job::Callback = + Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); + let popup = Popup::new("hover", contents).auto_close(true); + compositor.replace_or_push("hover", popup); + }); + Ok(call) + }; + + cx.jobs.callback(callback); + Ok(()) } From 0c14d9f869c503a873a420c4acab17bd922d1d3b Mon Sep 17 00:00:00 2001 From: zetashift Date: Wed, 19 Oct 2022 02:32:02 +0200 Subject: [PATCH 034/151] Add a first version of kanagawa theme (#4300) --- runtime/themes/kanagawa.toml | 125 +++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 runtime/themes/kanagawa.toml diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml new file mode 100644 index 0000000000000..50a7c0a787c93 --- /dev/null +++ b/runtime/themes/kanagawa.toml @@ -0,0 +1,125 @@ +# Kanagawa +# Author: zetashift + +# Adaptation of https://github.com/rebelot/kanagawa.nvim +# Original author: rebelot +# All credits to the original author, the palette is taken from the README +# because of some theming differences, it's not an exact copy of the original. + +## User interface +"ui.selection" = { bg = "waveBlue1" } +"ui.background" = { fg = "fujiWhite", bg = "sumiInk1" } + +"ui.linenr" = { fg = "sumiInk4" } + +"ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" } +"ui.statusline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } +"ui.statusline.normal" = { fg = "sumiInk0", bg = "crystalBlue", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen" } +"ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet" } + +"ui.bufferline" = { fg = "oldWhite", bg = "sumiInk0" } +"ui.bufferline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } + +"ui.popup" = { fg = "fujiWhite", bg = "sumiInk0" } +"ui.window" = { fg = "fujiWhite" } +"ui.help" = { fg = "fujiWhite", bg = "waveBlue1" } +"ui.text" = "fujiWhite" +"ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } +"ui.virtual" = "waveBlue1" + +"ui.cursor" = { fg = "fujiWhite", bg = "waveBlue1"} +"ui.cursor.primary" = { fg = "seaFoam", bg = "waveBlue1" } +"ui.cursor.match" = { fg = "seaFoam", modifiers = ["bold"] } +"ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" } + +diagnostic = { modifiers = ["underlined"] } + +error = "samuraiRed" +warning = "roninYellow" +info = "waveAqua1" +hint = "dragonBlue" + +## Syntax highlighting +"type" = "waveAqua2" +"constant" = "surimiOrange" +"constant.numeric" = "sakuraPink" +"constant.character.escape" = "springBlue" +"string" = "springGreen" +"string.regexp" = "boatYellow2" +"comment" = "fujiGray" +"variable" = "fujiWhite" +"variable.builtin" = "waveRed" +"variable.parameter" = "carpYellow" +"variable.other.member" = "carpYellow" +"label" = "springBlue" +"punctuation" = "springViolet2" +"punctuation.delimiter" = "springViolet2" +"punctuation.bracket" = "springViolet2" +"keyword" = "oniViolet" +"keyword.directive" = "peachRed" +"operator" = "boatYellow2" +"function" = "crystalBlue" +"function.builtin" = "peachRed" +"function.macro" = "waveRed" +"tag" = "springBlue" +"namespace" = "surimiOrange" +"attribute" = "peachRed" +"constructor" = "springBlue" +"module" = "waveAqua2" +"special" = "peachRed" + +## Markup modifiers +"markup.heading.marker" = "fujiGray" +"markup.heading.1" = { fg = "surimiOrange", modifiers = ["bold"] } +"markup.heading.2" = { fg = "carpYellow", modifiers = ["bold"] } +"markup.heading.3" = { fg = "waveAqua2", modifiers = ["bold"] } +"markup.heading.4" = { fg = "springGreen", modifiers = ["bold"] } +"markup.heading.5" = { fg = "waveRed", modifiers = ["bold"] } +"markup.heading.6" = { fg = "autumnRed", modifiers = ["bold"] } +"markup.list" = "oniViolet" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.link.url" = { fg = "springBlue", modifiers = ["underlined"] } +"markup.link.text" = "crystalBlue" +"markup.quote" = "seaFoam" +"markup.raw" = "seaFoam" + +[palette] +seaFoam = "#C7CCD1" # custom lighter foreground +fujiWhite = "#DCD7BA" # default foreground +oldWhite = "#C8C093" # dark foreground, e.g. statuslines +sumiInk0 = "#16161D" # dark background, e.g. statuslines, floating windows +sumiInk1 = "#1F1F28" # default background +sumiInk3 = "#363646" # lighter background, e.g. colorcolumns and folds +sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column +waveBlue1 = "#223249" # popup background, visual selection background +waveBlue2 = "#2D4F67" # popup selection background, search background +winterGreen = "#2B3328" # diff add background +winterYellow = "#49443C" # diff change background +winterRed = "#43242B" # diff deleted background +winterBlue = "#252535" # diff line background +autumnGreen = "#76946A" # git add +autumnRed = "#C34043" # git delete +autumnYellow = "#DCA561" # git change +samuraiRed = "#E82424" # diagnostic error +roninYellow = "#FF9E3B" # diagnostic warning +waveAqua1 = "#6A9589" # diagnostic info +dragonBlue = "#658594" # diagnostic hint +fujiGray = "#727169" # comments +springViolet1 = "#938AA9" # light foreground +oniViolet = "#957FB8" # statements and keywords +crystalBlue = "#7E9CD8" # functions and titles +springViolet2 = "#9CABCA" # brackets and punctuation +springBlue = "#7FB4CA" # specials and builtins +lightBlue = "#A3D4D5" # not used! +waveAqua2 = "#7AA89F" # types +springGreen = "#98BB6C" # strings +boatYellow1 = "#938056" # not used +boatYellow2 = "#C0A36E" # operators, regex +carpYellow = "#E6C384" # identifiers +sakuraPink = "#D27E99" # numbers +waveRed = "#E46876" # standout specials 1, e.g. builtin variables +peachRed = "#FF5D62" # standout specials 2, e.g. exception handling, returns +surimiOrange = "#FFA066" # constants, imports, booleans +katanaGray = "#717C7C" # deprecated \ No newline at end of file From 1a772d1b67d4895d79273b027b1b1f86bd1616fe Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Wed, 19 Oct 2022 08:53:58 +0800 Subject: [PATCH 035/151] Fix deleting word from end of buffer (#4328) --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 318dd035eea13..5073651b59355 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2885,7 +2885,7 @@ pub mod insert { /// Exclude the cursor in range. fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range { - if range.to() == cursor.to() { + if range.to() == cursor.to() && text.len_chars() != cursor.to() { Range::new( range.from(), graphemes::prev_grapheme_boundary(text, cursor.to()), From faf0c521d15c314f411cc6178024c5d3310212da Mon Sep 17 00:00:00 2001 From: Peter Phillips Date: Tue, 18 Oct 2022 22:16:44 -0400 Subject: [PATCH 036/151] Fix link to textobjects usage from keymap documentation (#4357) --- book/src/keymap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 5bdbdd2935b4a..6523b09fb88cb 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -221,7 +221,7 @@ Jumps to various locations. Accessed by typing `m` in [normal mode](#normal-mode). See the relevant section in [Usage](./usage.md) for an explanation about -[surround](./usage.md#surround) and [textobject](./usage.md#textobject) usage. +[surround](./usage.md#surround) and [textobject](./usage.md#textobjects) usage. | Key | Description | Command | | ----- | ----------- | ------- | From d706194597d462fbaeb1ef55e2e8fb6eae38d2f3 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 10 Apr 2022 11:05:47 -0400 Subject: [PATCH 037/151] chore(write): serialize write operations within a Document The way that document writes are handled are by submitting them to the async job pool, which are all executed opportunistically out of order. It was discovered that this can lead to write inconsistencies when there are multiple writes to the same file in quick succession. This seeks to fix this problem by removing document writes from the general pool of jobs and into its own specialized event. Now when a user submits a write with one of the write commands, a request is simply queued up in a new mpsc channel that each Document makes to handle its own writes. This way, if multiple writes are submitted on the same document, they are executed in order, while still allowing concurrent writes for different documents. --- helix-term/src/application.rs | 135 +++++++++++++++++++++++-------- helix-term/src/commands.rs | 2 +- helix-term/src/commands/typed.rs | 13 +-- helix-term/tests/test/write.rs | 2 - helix-view/src/document.rs | 128 ++++++++++++++++++++++++++--- helix-view/src/editor.rs | 81 ++++++++++++++++--- 6 files changed, 297 insertions(+), 64 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 4bb36b59cf4fa..a9e25d0891c08 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -6,7 +6,14 @@ use helix_core::{ pos_at_coords, syntax, Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; -use helix_view::{align_view, editor::ConfigEvent, theme, tree::Layout, Align, Editor}; +use helix_view::{ + align_view, + document::DocumentSaveEventResult, + editor::{ConfigEvent, EditorEvent}, + theme, + tree::Layout, + Align, Editor, +}; use serde_json::json; use crate::{ @@ -19,7 +26,7 @@ use crate::{ ui::{self, overlay::overlayed}, }; -use log::{error, warn}; +use log::{debug, error, warn}; use std::{ io::{stdin, stdout, Write}, sync::Arc, @@ -294,26 +301,6 @@ impl Application { Some(signal) = self.signals.next() => { self.handle_signals(signal).await; } - Some((id, call)) = self.editor.language_servers.incoming.next() => { - self.handle_language_server_message(call, id).await; - // limit render calls for fast language server messages - let last = self.editor.language_servers.incoming.is_empty(); - - if last || self.last_render.elapsed() > LSP_DEADLINE { - self.render(); - self.last_render = Instant::now(); - } - } - Some(payload) = self.editor.debugger_events.next() => { - let needs_render = self.editor.handle_debugger_message(payload).await; - if needs_render { - self.render(); - } - } - Some(config_event) = self.editor.config_events.1.recv() => { - self.handle_config_events(config_event); - self.render(); - } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); @@ -322,20 +309,47 @@ impl Application { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); } - _ = &mut self.editor.idle_timer => { - // idle timeout - self.editor.clear_idle_timer(); - self.handle_idle_timeout(); + event = self.editor.wait_event() => { + match event { + EditorEvent::DocumentSave(event) => { + self.handle_document_write(event); + self.render(); + } + EditorEvent::ConfigEvent(event) => { + self.handle_config_events(event); + self.render(); + } + EditorEvent::LanguageServerMessage((id, call)) => { + self.handle_language_server_message(call, id).await; + // limit render calls for fast language server messages + let last = self.editor.language_servers.incoming.is_empty(); + + if last || self.last_render.elapsed() > LSP_DEADLINE { + self.render(); + self.last_render = Instant::now(); + } + } + EditorEvent::DebuggerEvent(payload) => { + let needs_render = self.editor.handle_debugger_message(payload).await; + if needs_render { + self.render(); + } + } + EditorEvent::IdleTimer => { + self.editor.clear_idle_timer(); + self.handle_idle_timeout(); - #[cfg(feature = "integration")] - { - idle_handled = true; + #[cfg(feature = "integration")] + { + idle_handled = true; + } + } } } } // for integration tests only, reset the idle timer after every - // event to make a signal when test events are done processing + // event to signal when test events are done processing #[cfg(feature = "integration")] { if idle_handled { @@ -446,6 +460,46 @@ impl Application { } } + pub fn handle_document_write(&mut self, doc_save_event: DocumentSaveEventResult) { + if let Err(err) = doc_save_event { + self.editor.set_error(err.to_string()); + return; + } + + let doc_save_event = doc_save_event.unwrap(); + let doc = self.editor.document_mut(doc_save_event.doc_id); + + if doc.is_none() { + warn!( + "received document saved event for non-existent doc id: {}", + doc_save_event.doc_id + ); + + return; + } + + let doc = doc.unwrap(); + + debug!( + "document {:?} saved with revision {}", + doc.path(), + doc_save_event.revision + ); + + doc.set_last_saved_revision(doc_save_event.revision); + let lines = doc.text().len_lines(); + let bytes = doc.text().len_bytes(); + + let path_str = doc + .path() + .expect("document written without path") + .to_string_lossy() + .into_owned(); + + self.editor + .set_status(format!("'{}' written, {}L {}B", path_str, lines, bytes)); + } + pub fn handle_terminal_events(&mut self, event: Result) { let mut cx = crate::compositor::Context { editor: &mut self.editor, @@ -866,11 +920,28 @@ impl Application { self.event_loop(input_stream).await; - let err = self.close().await.err(); + let mut save_errs = Vec::new(); + + for doc in self.editor.documents_mut() { + if let Some(Err(err)) = doc.close().await { + save_errs.push(( + doc.path() + .map(|path| path.to_string_lossy().into_owned()) + .unwrap_or_else(|| "".into()), + err, + )); + } + } + let close_err = self.close().await.err(); restore_term()?; - if let Some(err) = err { + for (path, err) in save_errs { + self.editor.exit_code = 1; + eprintln!("Error closing '{}': {}", path, err); + } + + if let Some(err) = close_err { self.editor.exit_code = 1; eprintln!("Error: {}", err); } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5073651b59355..f38434e278ba2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -51,7 +51,7 @@ use crate::{ ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; -use crate::job::{self, Job, Jobs}; +use crate::job::{self, Jobs}; use futures_util::{FutureExt, StreamExt}; use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashSet, num::NonZeroUsize}; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1bfc81536d354..d82dd7fe6c377 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -77,7 +77,9 @@ fn buffer_close_by_ids_impl( let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids .iter() .filter_map(|&doc_id| { - if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) { + if let Err(CloseError::BufferModified(name)) = + helix_lsp::block_on(editor.close_document(doc_id, force)) + { Some((doc_id, name)) } else { None @@ -269,6 +271,7 @@ fn write_impl( doc.set_path(Some(path.as_ref().as_ref())) .context("invalid filepath")?; } + if doc.path().is_none() { bail!("cannot write a buffer without a filename"); } @@ -287,8 +290,8 @@ fn write_impl( } else { None }; - let future = doc.format_and_save(fmt, force); - cx.jobs.add(Job::new(future).wait_before_exiting()); + + doc.format_and_save(fmt, force)?; if path.is_some() { let id = doc.id(); @@ -602,8 +605,8 @@ fn write_all_impl( } else { None }; - let future = doc.format_and_save(fmt, force); - jobs.add(Job::new(future).wait_before_exiting()); + + doc.format_and_save(fmt, force)?; } if quit { diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 8869d881db1eb..4ac850c143573 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -62,7 +62,6 @@ async fn test_write_quit() -> anyhow::Result<()> { } #[tokio::test] -#[ignore] async fn test_write_concurrent() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); @@ -92,7 +91,6 @@ async fn test_write_concurrent() -> anyhow::Result<()> { } #[tokio::test] -#[ignore] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0daa983f65ca7..d6480b32d5130 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -3,6 +3,7 @@ use futures_util::future::BoxFuture; use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; use helix_core::Range; +use log::debug; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; @@ -13,6 +14,8 @@ use std::future::Future; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::Mutex; use helix_core::{ encoding, @@ -83,6 +86,16 @@ impl Serialize for Mode { } } +/// A snapshot of the text of a document that we want to write out to disk +#[derive(Debug, Clone)] +pub struct DocumentSaveEvent { + pub revision: usize, + pub doc_id: DocumentId, +} + +pub type DocumentSaveEventResult = Result; +pub type DocumentSaveEventFuture = BoxFuture<'static, DocumentSaveEventResult>; + pub struct Document { pub(crate) id: DocumentId, text: Rope, @@ -118,6 +131,9 @@ pub struct Document { last_saved_revision: usize, version: i32, // should be usize? pub(crate) modified_since_accessed: bool, + save_sender: Option>, + save_receiver: Option>, + current_save: Arc>>, diagnostics: Vec, language_server: Option>, @@ -338,6 +354,7 @@ impl Document { let encoding = encoding.unwrap_or(encoding::UTF_8); let changes = ChangeSet::new(&text); let old_state = None; + let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel(); Self { id: DocumentId::default(), @@ -358,6 +375,9 @@ impl Document { savepoint: None, last_saved_revision: 0, modified_since_accessed: false, + save_sender: Some(save_sender), + save_receiver: Some(save_receiver), + current_save: Arc::new(Mutex::new(None)), language_server: None, } } @@ -492,29 +512,34 @@ impl Document { Some(fut.boxed()) } - pub fn save(&mut self, force: bool) -> impl Future> { + pub fn save(&mut self, force: bool) -> Result<(), anyhow::Error> { self.save_impl::>(None, force) } pub fn format_and_save( &mut self, - formatting: Option>>, + formatting: Option< + impl Future> + 'static + Send, + >, force: bool, - ) -> impl Future> { + ) -> anyhow::Result<()> { self.save_impl(formatting, force) } - // TODO: do we need some way of ensuring two save operations on the same doc can't run at once? - // or is that handled by the OS/async layer + // TODO: impl Drop to handle ensuring writes when closed /// The `Document`'s text is encoded according to its encoding and written to the file located /// at its `path()`. /// /// If `formatting` is present, it supplies some changes that we apply to the text before saving. - fn save_impl>>( + fn save_impl> + 'static + Send>( &mut self, formatting: Option, force: bool, - ) -> impl Future> { + ) -> Result<(), anyhow::Error> { + if self.save_sender.is_none() { + bail!("saves are closed for this document!"); + } + // we clone and move text + path into the future so that we asynchronously save the current // state without blocking any further edits. @@ -525,12 +550,13 @@ impl Document { let language_server = self.language_server.clone(); // mark changes up to now as saved - self.reset_modified(); + let current_rev = self.get_current_revision(); + let doc_id = self.id(); let encoding = self.encoding; // We encode the file according to the `Document`'s encoding. - async move { + let save_event = async move { use tokio::fs::File; if let Some(parent) = path.parent() { // TODO: display a prompt asking the user if the directories should be created @@ -563,9 +589,14 @@ impl Document { let mut file = File::create(path).await?; to_writer(&mut file, encoding, &text).await?; + let event = DocumentSaveEvent { + revision: current_rev, + doc_id, + }; + if let Some(language_server) = language_server { if !language_server.is_initialized() { - return Ok(()); + return Ok(event); } if let Some(notification) = language_server.text_document_did_save(identifier, &text) @@ -574,8 +605,70 @@ impl Document { } } - Ok(()) + Ok(event) + }; + + self.save_sender + .as_mut() + .unwrap() + .send(Box::pin(save_event)) + .map_err(|err| anyhow!("failed to send save event: {}", err)) + } + + pub async fn await_save(&mut self) -> Option { + let mut current_save = self.current_save.lock().await; + if let Some(ref mut save) = *current_save { + let result = save.await; + *current_save = None; + debug!("save of '{:?}' result: {:?}", self.path(), result); + return Some(result); + } + + // return early if the receiver is closed + self.save_receiver.as_ref()?; + + let save = match self.save_receiver.as_mut().unwrap().recv().await { + Some(save) => save, + None => { + self.save_receiver = None; + return None; + } + }; + + // save a handle to the future so that when a poll on this + // function gets cancelled, we don't lose it + *current_save = Some(save); + debug!("awaiting save of '{:?}'", self.path()); + + let result = (*current_save).as_mut().unwrap().await; + *current_save = None; + + debug!("save of '{:?}' result: {:?}", self.path(), result); + + Some(result) + } + + /// Prepares the Document for being closed by stopping any new writes + /// and flushing through the queue of pending writes. If any fail, + /// it stops early before emptying the rest of the queue. Callers + /// should keep calling until it returns None. + pub async fn close(&mut self) -> Option { + if self.save_sender.is_some() { + self.save_sender = None; } + + let mut final_result = None; + + while let Some(save_event) = self.await_save().await { + let is_err = save_event.is_err(); + final_result = Some(save_event); + + if is_err { + break; + } + } + + final_result } /// Detect the programming language based on the file type. @@ -941,6 +1034,19 @@ impl Document { self.last_saved_revision = current_revision; } + /// Set the document's latest saved revision to the given one. + pub fn set_last_saved_revision(&mut self, rev: usize) { + self.last_saved_revision = rev; + } + + /// Get the current revision number + pub fn get_current_revision(&mut self) -> usize { + let history = self.history.take(); + let current_revision = history.current_revision(); + self.history.set(history); + current_revision + } + /// Corresponding language scope name. Usually `source.`. pub fn language_scope(&self) -> Option<&str> { self.language diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e9a3c63970176..ec6119a4ff549 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,6 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, - document::Mode, + document::{DocumentSaveEventResult, Mode}, graphics::{CursorKind, Rect}, info::Info, input::KeyEvent, @@ -9,8 +9,9 @@ use crate::{ Document, DocumentId, View, ViewId, }; -use futures_util::future; -use futures_util::stream::select_all::SelectAll; +use futures_util::stream::{select_all::SelectAll, FuturesUnordered}; +use futures_util::{future, StreamExt}; +use helix_lsp::Call; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ @@ -65,7 +66,7 @@ where ) } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct FilePickerConfig { /// IgnoreOptions @@ -172,7 +173,7 @@ pub struct Config { pub color_modes: bool, } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct TerminalConfig { pub command: String, @@ -225,7 +226,7 @@ pub fn get_terminal_provider() -> Option { None } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct LspConfig { /// Display LSP progress messages below statusline @@ -246,7 +247,7 @@ impl Default for LspConfig { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct SearchConfig { /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. @@ -255,7 +256,7 @@ pub struct SearchConfig { pub wrap_around: bool, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct StatusLineConfig { pub left: Vec, @@ -279,7 +280,7 @@ impl Default for StatusLineConfig { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct ModeConfig { pub normal: String, @@ -458,7 +459,7 @@ impl std::str::FromStr for GutterType { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct WhitespaceConfig { pub render: WhitespaceRender, @@ -688,6 +689,15 @@ pub struct Editor { pub config_events: (UnboundedSender, UnboundedReceiver), } +#[derive(Debug)] +pub enum EditorEvent { + DocumentSave(DocumentSaveEventResult), + ConfigEvent(ConfigEvent), + LanguageServerMessage((usize, Call)), + DebuggerEvent(dap::Payload), + IdleTimer, +} + #[derive(Debug, Clone)] pub enum ConfigEvent { Refresh, @@ -719,6 +729,8 @@ pub enum CloseError { DoesNotExist, /// Buffer is modified BufferModified(String), + /// Document failed to save + SaveError(anyhow::Error), } impl Editor { @@ -1079,8 +1091,12 @@ impl Editor { self._refresh(); } - pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> { - let doc = match self.documents.get(&doc_id) { + pub async fn close_document( + &mut self, + doc_id: DocumentId, + force: bool, + ) -> Result<(), CloseError> { + let doc = match self.documents.get_mut(&doc_id) { Some(doc) => doc, None => return Err(CloseError::DoesNotExist), }; @@ -1089,8 +1105,19 @@ impl Editor { return Err(CloseError::BufferModified(doc.display_name().into_owned())); } + if let Some(Err(err)) = doc.close().await { + return Err(CloseError::SaveError(err)); + } + + // Don't fail the whole write because the language server could not + // acknowledge the close if let Some(language_server) = doc.language_server() { - tokio::spawn(language_server.text_document_did_close(doc.identifier())); + if let Err(err) = language_server + .text_document_did_close(doc.identifier()) + .await + { + log::error!("Error closing doc in language server: {}", err); + } } enum Action { @@ -1269,4 +1296,32 @@ impl Editor { .await .map(|_| ()) } + + pub async fn wait_event(&mut self) -> EditorEvent { + let mut saves: FuturesUnordered<_> = self + .documents + .values_mut() + .map(Document::await_save) + .collect(); + + tokio::select! { + biased; + + Some(Some(event)) = saves.next() => { + EditorEvent::DocumentSave(event) + } + Some(config_event) = self.config_events.1.recv() => { + EditorEvent::ConfigEvent(config_event) + } + Some(message) = self.language_servers.incoming.next() => { + EditorEvent::LanguageServerMessage(message) + } + Some(event) = self.debugger_events.next() => { + EditorEvent::DebuggerEvent(event) + } + _ = &mut self.idle_timer => { + EditorEvent::IdleTimer + } + } + } } From a5a93182cd5ccf88bc95b68044aa05d746ded35e Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 23 Apr 2022 18:38:55 -0400 Subject: [PATCH 038/151] fix: buffer-close ensuring writes Make sure buffer-close waits for the document to finish its writes. --- helix-term/src/application.rs | 2 + helix-term/src/commands/typed.rs | 1 + helix-term/tests/test/commands.rs | 1 - helix-view/src/document.rs | 66 +++++++++++++++++++++++++------ helix-view/src/editor.rs | 5 +++ 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a9e25d0891c08..2c1047da585a8 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -310,6 +310,8 @@ impl Application { self.render(); } event = self.editor.wait_event() => { + log::debug!("received editor event: {:?}", event); + match event { EditorEvent::DocumentSave(event) => { self.handle_document_write(event); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index d82dd7fe6c377..375e7b4f25a85 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -151,6 +151,7 @@ fn buffer_close( } let document_ids = buffer_gather_paths_impl(cx.editor, args); + log::debug!("closing buffers: {:?}", document_ids); buffer_close_by_ids_impl(cx.editor, &document_ids, false) } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index f7ce9af081710..8aea144bd5ca5 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -26,7 +26,6 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { } #[tokio::test] -#[ignore] async fn test_buffer_close_concurrent() -> anyhow::Result<()> { test_key_sequences( &mut Application::new(Args::default(), Config::default())?, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index d6480b32d5130..3045e3b7b946f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -616,6 +616,10 @@ impl Document { } pub async fn await_save(&mut self) -> Option { + self.await_save_impl(true).await + } + + async fn await_save_impl(&mut self, block: bool) -> Option { let mut current_save = self.current_save.lock().await; if let Some(ref mut save) = *current_save { let result = save.await; @@ -627,7 +631,15 @@ impl Document { // return early if the receiver is closed self.save_receiver.as_ref()?; - let save = match self.save_receiver.as_mut().unwrap().recv().await { + let rx = self.save_receiver.as_mut().unwrap(); + + let save_req = if block { + rx.recv().await + } else { + rx.try_recv().ok() + }; + + let save = match save_req { Some(save) => save, None => { self.save_receiver = None; @@ -648,19 +660,24 @@ impl Document { Some(result) } - /// Prepares the Document for being closed by stopping any new writes - /// and flushing through the queue of pending writes. If any fail, - /// it stops early before emptying the rest of the queue. Callers - /// should keep calling until it returns None. - pub async fn close(&mut self) -> Option { - if self.save_sender.is_some() { - self.save_sender = None; - } + /// Flushes the queue of pending writes. If any fail, + /// it stops early before emptying the rest of the queue. + pub async fn try_flush_saves(&mut self) -> Option { + self.flush_saves_impl(false).await + } + async fn flush_saves_impl(&mut self, block: bool) -> Option { let mut final_result = None; - while let Some(save_event) = self.await_save().await { - let is_err = save_event.is_err(); + while let Some(save_event) = self.await_save_impl(block).await { + let is_err = match &save_event { + Ok(event) => { + self.set_last_saved_revision(event.revision); + false + } + Err(_) => true, + }; + final_result = Some(save_event); if is_err { @@ -671,6 +688,17 @@ impl Document { final_result } + /// Prepares the Document for being closed by stopping any new writes + /// and flushing through the queue of pending writes. If any fail, + /// it stops early before emptying the rest of the queue. + pub async fn close(&mut self) -> Option { + if self.save_sender.is_some() { + self.save_sender = None; + } + + self.flush_saves_impl(true).await + } + /// Detect the programming language based on the file type. pub fn detect_language(&mut self, config_loader: Arc) { if let Some(path) = &self.path { @@ -1023,6 +1051,11 @@ impl Document { let history = self.history.take(); let current_revision = history.current_revision(); self.history.set(history); + log::debug!( + "modified - last saved: {}, current: {}", + self.last_saved_revision, + current_revision + ); current_revision != self.last_saved_revision || !self.changes.is_empty() } @@ -1036,9 +1069,20 @@ impl Document { /// Set the document's latest saved revision to the given one. pub fn set_last_saved_revision(&mut self, rev: usize) { + log::debug!( + "doc {} revision updated {} -> {}", + self.id, + self.last_saved_revision, + rev + ); self.last_saved_revision = rev; } + /// Get the document's latest saved revision. + pub fn get_last_saved_revision(&mut self) -> usize { + self.last_saved_revision + } + /// Get the current revision number pub fn get_current_revision(&mut self) -> usize { let history = self.history.take(); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ec6119a4ff549..e038a82de7648 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1101,6 +1101,11 @@ impl Editor { None => return Err(CloseError::DoesNotExist), }; + // flush out any pending writes first to clear the modified status + if let Some(save_result) = doc.try_flush_saves().await { + save_result?; + } + if !force && doc.is_modified() { return Err(CloseError::BufferModified(doc.display_name().into_owned())); } From 83b6042b97d13fca751e3d5d0c32f04e3ad04c9a Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 24 Apr 2022 15:33:43 -0400 Subject: [PATCH 039/151] fix(write): do not set new path on document until write succeeds If a document is written with a new path, currently, in the event that the write fails, the document still gets its path changed. This fixes it so that the path is not updated unless the write succeeds. --- helix-term/src/application.rs | 26 +++++++++----- helix-term/src/commands/typed.rs | 9 ++--- helix-term/tests/test/write.rs | 61 ++++++++++++++++++++++++++++++-- helix-view/src/document.rs | 52 +++++++++++++++++++-------- 4 files changed, 116 insertions(+), 32 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2c1047da585a8..0640de3c4505d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -3,6 +3,7 @@ use futures_util::Stream; use helix_core::{ config::{default_syntax_loader, user_syntax_loader}, diagnostic::{DiagnosticTag, NumberOrString}, + path::get_relative_path, pos_at_coords, syntax, Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; @@ -489,17 +490,26 @@ impl Application { ); doc.set_last_saved_revision(doc_save_event.revision); + let lines = doc.text().len_lines(); let bytes = doc.text().len_bytes(); - let path_str = doc - .path() - .expect("document written without path") - .to_string_lossy() - .into_owned(); - - self.editor - .set_status(format!("'{}' written, {}L {}B", path_str, lines, bytes)); + if let Err(err) = doc.set_path(Some(&doc_save_event.path)) { + log::error!( + "error setting path for doc '{:?}': {}", + doc.path(), + err.to_string(), + ); + self.editor.set_error(err.to_string()); + } else { + // TODO: fix being overwritten by lsp + self.editor.set_status(format!( + "'{}' written, {}L {}B", + get_relative_path(&doc_save_event.path).to_string_lossy(), + lines, + bytes + )); + } } pub fn handle_terminal_events(&mut self, event: Result) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 375e7b4f25a85..35c84601636a5 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -268,11 +268,6 @@ fn write_impl( let jobs = &mut cx.jobs; let doc = doc_mut!(cx.editor); - if let Some(ref path) = path { - doc.set_path(Some(path.as_ref().as_ref())) - .context("invalid filepath")?; - } - if doc.path().is_none() { bail!("cannot write a buffer without a filename"); } @@ -292,7 +287,7 @@ fn write_impl( None }; - doc.format_and_save(fmt, force)?; + doc.format_and_save(fmt, path.map(AsRef::as_ref), force)?; if path.is_some() { let id = doc.id(); @@ -607,7 +602,7 @@ fn write_all_impl( None }; - doc.format_and_save(fmt, force)?; + doc.format_and_save::<_, PathBuf>(fmt, None, force)?; } if quit { diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 4ac850c143573..d2e6922f2740e 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -35,7 +35,7 @@ async fn test_write() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_quit() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; @@ -129,7 +129,64 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { } #[tokio::test] -#[ignore] +async fn test_write_new_path() -> anyhow::Result<()> { + let mut file1 = tempfile::NamedTempFile::new().unwrap(); + let mut file2 = tempfile::NamedTempFile::new().unwrap(); + + test_key_sequences( + &mut Application::new( + Args { + files: vec![(file1.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + vec![ + ( + Some("ii can eat glass, it will not hurt me:w"), + Some(&|app| { + let doc = doc!(app.editor); + assert!(!app.editor.is_err()); + assert_eq!(file1.path(), doc.path().unwrap()); + }), + ), + ( + Some(&format!(":w {}", file2.path().to_string_lossy())), + Some(&|app| { + let doc = doc!(app.editor); + assert!(!app.editor.is_err()); + assert_eq!(file2.path(), doc.path().unwrap()); + assert!(app.editor.document_by_path(file1.path()).is_none()); + }), + ), + ], + false, + ) + .await?; + + file1.as_file_mut().flush()?; + file1.as_file_mut().sync_all()?; + file2.as_file_mut().flush()?; + file2.as_file_mut().sync_all()?; + + let mut file1_content = String::new(); + file1.as_file_mut().read_to_string(&mut file1_content)?; + assert_eq!( + helpers::platform_line("i can eat glass, it will not hurt me\n"), + file1_content + ); + + let mut file2_content = String::new(); + file2.as_file_mut().read_to_string(&mut file2_content)?; + assert_eq!( + helpers::platform_line("i can eat glass, it will not hurt me\n"), + file2_content + ); + + Ok(()) +} + +#[tokio::test] async fn test_write_fail_new_path() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3045e3b7b946f..82d526a9c5b3e 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -91,6 +91,7 @@ impl Serialize for Mode { pub struct DocumentSaveEvent { pub revision: usize, pub doc_id: DocumentId, + pub path: PathBuf, } pub type DocumentSaveEventResult = Result; @@ -512,41 +513,61 @@ impl Document { Some(fut.boxed()) } - pub fn save(&mut self, force: bool) -> Result<(), anyhow::Error> { - self.save_impl::>(None, force) + pub fn save>( + &mut self, + path: Option

, + force: bool, + ) -> Result<(), anyhow::Error> { + self.save_impl::, _>(None, path, force) } - pub fn format_and_save( + pub fn format_and_save( &mut self, - formatting: Option< - impl Future> + 'static + Send, - >, + formatting: Option, + path: Option

, force: bool, - ) -> anyhow::Result<()> { - self.save_impl(formatting, force) + ) -> anyhow::Result<()> + where + F: Future> + 'static + Send, + P: Into, + { + self.save_impl(formatting, path, force) } - // TODO: impl Drop to handle ensuring writes when closed /// The `Document`'s text is encoded according to its encoding and written to the file located /// at its `path()`. /// /// If `formatting` is present, it supplies some changes that we apply to the text before saving. - fn save_impl> + 'static + Send>( + fn save_impl( &mut self, formatting: Option, + path: Option

, force: bool, - ) -> Result<(), anyhow::Error> { + ) -> Result<(), anyhow::Error> + where + F: Future> + 'static + Send, + P: Into, + { if self.save_sender.is_none() { bail!("saves are closed for this document!"); } // we clone and move text + path into the future so that we asynchronously save the current // state without blocking any further edits. - let mut text = self.text().clone(); - let path = self.path.clone().expect("Can't save with no path set!"); - let identifier = self.identifier(); + let path = match path { + Some(path) => helix_core::path::get_canonicalized_path(&path.into())?, + None => { + if self.path.is_none() { + bail!("Can't save with no path set!"); + } + + self.path.as_ref().unwrap().clone() + } + }; + + let identifier = self.identifier(); let language_server = self.language_server.clone(); // mark changes up to now as saved @@ -586,12 +607,13 @@ impl Document { } } - let mut file = File::create(path).await?; + let mut file = File::create(&path).await?; to_writer(&mut file, encoding, &text).await?; let event = DocumentSaveEvent { revision: current_rev, doc_id, + path, }; if let Some(language_server) = language_server { From e1f7bdb1d2ef68c0de38e768080291901ff4662e Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 9 May 2022 23:08:12 -0400 Subject: [PATCH 040/151] fix buffer-close --- helix-term/src/application.rs | 1 + helix-term/src/commands/typed.rs | 6 +++--- helix-term/tests/test/commands.rs | 2 +- helix-term/tests/test/helpers.rs | 6 ++++-- helix-term/tests/test/write.rs | 2 +- helix-view/src/document.rs | 14 ++++++-------- helix-view/src/editor.rs | 4 ++-- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 0640de3c4505d..5c25e8aa2984c 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -344,6 +344,7 @@ impl Application { #[cfg(feature = "integration")] { + log::debug!("idle handled"); idle_handled = true; } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 35c84601636a5..efe693b9d981b 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -77,9 +77,9 @@ fn buffer_close_by_ids_impl( let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids .iter() .filter_map(|&doc_id| { - if let Err(CloseError::BufferModified(name)) = + if let Err(CloseError::BufferModified(name)) = tokio::task::block_in_place(|| { helix_lsp::block_on(editor.close_document(doc_id, force)) - { + }) { Some((doc_id, name)) } else { None @@ -151,7 +151,6 @@ fn buffer_close( } let document_ids = buffer_gather_paths_impl(cx.editor, args); - log::debug!("closing buffers: {:?}", document_ids); buffer_close_by_ids_impl(cx.editor, &document_ids, false) } @@ -519,6 +518,7 @@ fn write_quit( } write_impl(cx, args.first(), false)?; + // TODO: change to use document close helix_lsp::block_on(cx.jobs.finish())?; quit(cx, &[], event) } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 8aea144bd5ca5..1f1bd6a95707b 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -25,7 +25,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_buffer_close_concurrent() -> anyhow::Result<()> { test_key_sequences( &mut Application::new(Args::default(), Config::default())?, diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 8f2501e61b9c6..bbcc6632e40e6 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -56,7 +56,9 @@ pub async fn test_key_sequences( for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() { if let Some(in_keys) = in_keys { for key_event in parse_macro(in_keys)?.into_iter() { - tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + let key = Event::Key(KeyEvent::from(key_event)); + log::trace!("sending key: {:?}", key); + tx.send(Ok(key))?; } } @@ -70,7 +72,7 @@ pub async fn test_key_sequences( // verify if it exited on the last iteration if it should have and // the inverse if i == num_inputs - 1 && app_exited != should_exit { - bail!("expected app to exit: {} != {}", app_exited, should_exit); + bail!("expected app to exit: {} != {}", should_exit, app_exited); } if let Some(test) = test_fn { diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index d2e6922f2740e..544f1ba19896a 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -61,7 +61,7 @@ async fn test_write_quit() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_concurrent() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 82d526a9c5b3e..b6e42065ec0a7 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -3,7 +3,6 @@ use futures_util::future::BoxFuture; use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; use helix_core::Range; -use log::debug; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; @@ -644,16 +643,15 @@ impl Document { async fn await_save_impl(&mut self, block: bool) -> Option { let mut current_save = self.current_save.lock().await; if let Some(ref mut save) = *current_save { + log::trace!("reawaiting save of '{:?}'", self.path()); let result = save.await; *current_save = None; - debug!("save of '{:?}' result: {:?}", self.path(), result); + log::trace!("reawait save of '{:?}' result: {:?}", self.path(), result); return Some(result); } // return early if the receiver is closed - self.save_receiver.as_ref()?; - - let rx = self.save_receiver.as_mut().unwrap(); + let rx = self.save_receiver.as_mut()?; let save_req = if block { rx.recv().await @@ -672,12 +670,12 @@ impl Document { // save a handle to the future so that when a poll on this // function gets cancelled, we don't lose it *current_save = Some(save); - debug!("awaiting save of '{:?}'", self.path()); + log::trace!("awaiting save of '{:?}'", self.path()); let result = (*current_save).as_mut().unwrap().await; *current_save = None; - debug!("save of '{:?}' result: {:?}", self.path(), result); + log::trace!("save of '{:?}' result: {:?}", self.path(), result); Some(result) } @@ -715,7 +713,7 @@ impl Document { /// it stops early before emptying the rest of the queue. pub async fn close(&mut self) -> Option { if self.save_sender.is_some() { - self.save_sender = None; + self.save_sender.take(); } self.flush_saves_impl(true).await diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e038a82de7648..e54aa7fa5ee1e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1102,8 +1102,8 @@ impl Editor { }; // flush out any pending writes first to clear the modified status - if let Some(save_result) = doc.try_flush_saves().await { - save_result?; + if let Some(Err(err)) = doc.try_flush_saves().await { + return Err(CloseError::SaveError(err)); } if !force && doc.is_modified() { From 69c9e44ef205a81c112dfb14d5f2e67e5ce9c300 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 10 May 2022 23:41:44 -0400 Subject: [PATCH 041/151] update write-quit to wait for saves --- helix-term/src/commands/typed.rs | 8 ++++++-- helix-term/tests/test/commands.rs | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index efe693b9d981b..bc254146dd186 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -518,8 +518,12 @@ fn write_quit( } write_impl(cx, args.first(), false)?; - // TODO: change to use document close - helix_lsp::block_on(cx.jobs.finish())?; + let doc = doc_mut!(cx.editor); + + tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) + .map(|result| result.map(|_| ())) + .unwrap_or(Ok(()))?; + quit(cx, &[], event) } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 1f1bd6a95707b..b7c0f7cc4b383 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -8,7 +8,7 @@ use helix_term::application::Application; use super::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_quit_fail() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; @@ -16,6 +16,11 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { &mut helpers::app_with_file(file.path())?, Some("ihello:wq"), Some(&|app| { + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path)); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), false, From b8a07f7d15a10186fa2b481a3423c23f32d7d561 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 24 Jun 2022 08:39:07 -0400 Subject: [PATCH 042/151] add conditional noop render back It makes it much slower without stubbing this out --- helix-core/src/auto_pairs.rs | 1 - helix-term/src/application.rs | 4 ++++ helix-term/src/commands.rs | 14 ++------------ helix-term/src/commands/typed.rs | 17 +++-------------- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index ff680a77150e1..edc404ac5ccf8 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -7,7 +7,6 @@ use std::collections::HashMap; use smallvec::SmallVec; // Heavily based on https://github.com/codemirror/closebrackets/ - pub const DEFAULT_PAIRS: &[(char, char)] = &[ ('(', ')'), ('{', '}'), diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 5c25e8aa2984c..fd9b7c3e5da6b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -253,6 +253,10 @@ impl Application { Ok(app) } + #[cfg(feature = "integration")] + fn render(&mut self) {} + + #[cfg(not(feature = "integration"))] fn render(&mut self) { let compositor = &mut self.compositor; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f38434e278ba2..a4421f034fdc5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2504,13 +2504,6 @@ fn insert_at_line_end(cx: &mut Context) { doc.set_selection(view.id, selection); } -/// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for -/// example because we just applied the same changes while saving. -enum Modified { - SetUnmodified, - LeaveModified, -} - // Creates an LspCallback that waits for formatting changes to be computed. When they're done, // it applies them, but only if the doc hasn't changed. // @@ -2519,7 +2512,6 @@ enum Modified { async fn make_format_callback( doc_id: DocumentId, doc_version: i32, - modified: Modified, format: impl Future> + Send + 'static, ) -> anyhow::Result { let format = format.await?; @@ -2536,17 +2528,15 @@ async fn make_format_callback( doc.append_changes_to_history(view.id); doc.detect_indent_and_line_ending(); view.ensure_cursor_in_view(doc, scrolloff); - if let Modified::SetUnmodified = modified { - doc.reset_modified(); - } } else { log::info!("discarded formatting changes because the document changed"); } }); + Ok(call) } -#[derive(PartialEq)] +#[derive(PartialEq, Eq)] pub enum Open { Below, Above, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index bc254146dd186..14b23f2ad4eff 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -273,12 +273,7 @@ fn write_impl( let fmt = if auto_format { doc.auto_format().map(|fmt| { let shared = fmt.shared(); - let callback = make_format_callback( - doc.id(), - doc.version(), - Modified::SetUnmodified, - shared.clone(), - ); + let callback = make_format_callback(doc.id(), doc.version(), shared.clone()); jobs.callback(callback); shared }) @@ -346,8 +341,7 @@ fn format( let doc = doc!(cx.editor); if let Some(format) = doc.format() { - let callback = - make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format); + let callback = make_format_callback(doc.id(), doc.version(), format); cx.jobs.callback(callback); } @@ -593,12 +587,7 @@ fn write_all_impl( let fmt = if auto_format { doc.auto_format().map(|fmt| { let shared = fmt.shared(); - let callback = make_format_callback( - doc.id(), - doc.version(), - Modified::SetUnmodified, - shared.clone(), - ); + let callback = make_format_callback(doc.id(), doc.version(), shared.clone()); jobs.callback(callback); shared }) From cb23399dee723cec67f1a04dbe6514dfddfd7f5f Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 9 Jul 2022 22:39:40 -0400 Subject: [PATCH 043/151] improve reliability of shutdown --- helix-term/src/application.rs | 72 +++++++++++++++++++++++--------- helix-term/src/commands/typed.rs | 8 +++- helix-view/src/document.rs | 17 +++++++- helix-view/src/editor.rs | 8 +++- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index fd9b7c3e5da6b..e84739cd848a4 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -937,26 +937,26 @@ impl Application { self.event_loop(input_stream).await; - let mut save_errs = Vec::new(); - - for doc in self.editor.documents_mut() { - if let Some(Err(err)) = doc.close().await { - save_errs.push(( - doc.path() - .map(|path| path.to_string_lossy().into_owned()) - .unwrap_or_else(|| "".into()), - err, - )); - } - } + // let mut save_errs = Vec::new(); + + // for doc in self.editor.documents_mut() { + // if let Some(Err(err)) = doc.close().await { + // save_errs.push(( + // doc.path() + // .map(|path| path.to_string_lossy().into_owned()) + // .unwrap_or_else(|| "".into()), + // err, + // )); + // } + // } let close_err = self.close().await.err(); restore_term()?; - for (path, err) in save_errs { - self.editor.exit_code = 1; - eprintln!("Error closing '{}': {}", path, err); - } + // for (path, err) in save_errs { + // self.editor.exit_code = 1; + // eprintln!("Error closing '{}': {}", path, err); + // } if let Some(err) = close_err { self.editor.exit_code = 1; @@ -967,12 +967,44 @@ impl Application { } pub async fn close(&mut self) -> anyhow::Result<()> { - self.jobs.finish().await?; + // [NOTE] we intentionally do not return early for errors because we + // want to try to run as much cleanup as we can, regardless of + // errors along the way - if self.editor.close_language_servers(None).await.is_err() { - log::error!("Timed out waiting for language servers to shutdown"); + let mut result = match self.jobs.finish().await { + Ok(_) => Ok(()), + Err(err) => { + log::error!("Error executing job: {}", err); + Err(err) + } }; - Ok(()) + for doc in self.editor.documents_mut() { + if let Some(save_result) = doc.close().await { + result = match save_result { + Ok(_) => result, + Err(err) => { + if let Some(path) = doc.path() { + log::error!( + "Error saving document '{}': {}", + path.to_string_lossy(), + err + ); + } + Err(err) + } + }; + } + } + + match self.editor.close_language_servers(None).await { + Ok(_) => result, + Err(_) => { + log::error!("Timed out waiting for language servers to shutdown"); + Err(anyhow::format_err!( + "Timed out waiting for language servers to shutdown" + )) + } + } } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 14b23f2ad4eff..650ff75dccf75 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,5 +1,7 @@ use std::ops::Deref; +use crate::job::Job; + use super::*; use helix_view::{ @@ -19,6 +21,8 @@ pub struct TypableCommand { } fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + log::info!("quitting..."); + if event != PromptEvent::Validate { return Ok(()); } @@ -274,7 +278,7 @@ fn write_impl( doc.auto_format().map(|fmt| { let shared = fmt.shared(); let callback = make_format_callback(doc.id(), doc.version(), shared.clone()); - jobs.callback(callback); + jobs.add(Job::with_callback(callback).wait_before_exiting()); shared }) } else { @@ -512,8 +516,10 @@ fn write_quit( } write_impl(cx, args.first(), false)?; + let doc = doc_mut!(cx.editor); + tokio::task::block_in_place(|| helix_lsp::block_on(cx.jobs.finish()))?; tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) .map(|result| result.map(|_| ())) .unwrap_or(Ok(()))?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b6e42065ec0a7..1743fac2b3b9c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -551,6 +551,11 @@ impl Document { bail!("saves are closed for this document!"); } + log::debug!( + "submitting save of doc '{:?}'", + self.path().map(|path| path.to_string_lossy()) + ); + // we clone and move text + path into the future so that we asynchronously save the current // state without blocking any further edits. let mut text = self.text().clone(); @@ -695,7 +700,14 @@ impl Document { self.set_last_saved_revision(event.revision); false } - Err(_) => true, + Err(err) => { + log::error!( + "error saving document {:?}: {}", + self.path().map(|path| path.to_string_lossy()), + err + ); + true + } }; final_result = Some(save_event); @@ -1072,7 +1084,8 @@ impl Document { let current_revision = history.current_revision(); self.history.set(history); log::debug!( - "modified - last saved: {}, current: {}", + "id {} modified - last saved: {}, current: {}", + self.id, self.last_saved_revision, current_revision ); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e54aa7fa5ee1e..58fcf238fe547 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -816,12 +816,16 @@ impl Editor { #[inline] pub fn set_status>>(&mut self, status: T) { - self.status_msg = Some((status.into(), Severity::Info)); + let status = status.into(); + log::debug!("editor status: {}", status); + self.status_msg = Some((status, Severity::Info)); } #[inline] pub fn set_error>>(&mut self, error: T) { - self.status_msg = Some((error.into(), Severity::Error)); + let error = error.into(); + log::error!("editor error: {}", error); + self.status_msg = Some((error, Severity::Error)); } #[inline] From c9418582d2a6d8dbb8b5bb1d3432a9087438e61d Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 5 Jul 2022 00:15:15 -0400 Subject: [PATCH 044/151] fix modified status with auto format --- helix-term/src/commands.rs | 17 +++++++++++++++- helix-term/src/commands/typed.rs | 35 +++++++++++++++++--------------- helix-view/src/document.rs | 1 + 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a4421f034fdc5..e76e0280c9d4c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -52,7 +52,7 @@ use crate::{ }; use crate::job::{self, Jobs}; -use futures_util::{FutureExt, StreamExt}; +use futures_util::StreamExt; use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashSet, num::NonZeroUsize}; @@ -2513,6 +2513,7 @@ async fn make_format_callback( doc_id: DocumentId, doc_version: i32, format: impl Future> + Send + 'static, + write: Option<(Option, bool)>, ) -> anyhow::Result { let format = format.await?; let call: job::Callback = Box::new(move |editor, _compositor| { @@ -2523,11 +2524,25 @@ async fn make_format_callback( let scrolloff = editor.config().scrolloff; let doc = doc_mut!(editor, &doc_id); let view = view_mut!(editor); + let loader = editor.syn_loader.clone(); + if doc.version() == doc_version { apply_transaction(&format, doc, view); doc.append_changes_to_history(view.id); doc.detect_indent_and_line_ending(); view.ensure_cursor_in_view(doc, scrolloff); + + if let Some((path, force)) = write { + let refresh_lang = path.is_some(); + + if let Err(err) = doc.save(path, force) { + editor.set_error(format!("Error saving: {}", err)); + } else if refresh_lang { + let id = doc.id(); + doc.detect_language(loader); + let _ = editor.refresh_language_server(id); + } + } } else { log::info!("discarded formatting changes because the document changed"); } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 650ff75dccf75..955b3b5887f53 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -267,30 +267,32 @@ fn write_impl( path: Option<&Cow>, force: bool, ) -> anyhow::Result<()> { - let auto_format = cx.editor.config().auto_format; + let editor_auto_fmt = cx.editor.config().auto_format; let jobs = &mut cx.jobs; let doc = doc_mut!(cx.editor); + let path = path.map(AsRef::as_ref); if doc.path().is_none() { bail!("cannot write a buffer without a filename"); } - let fmt = if auto_format { + + let fmt = if editor_auto_fmt { doc.auto_format().map(|fmt| { - let shared = fmt.shared(); - let callback = make_format_callback(doc.id(), doc.version(), shared.clone()); + let callback = make_format_callback( + doc.id(), + doc.version(), + fmt, + Some((path.map(Into::into), force)), + ); + jobs.add(Job::with_callback(callback).wait_before_exiting()); - shared }) } else { None }; - doc.format_and_save(fmt, path.map(AsRef::as_ref), force)?; - - if path.is_some() { - let id = doc.id(); - doc.detect_language(cx.editor.syn_loader.clone()); - let _ = cx.editor.refresh_language_server(id); + if fmt.is_none() { + doc.save(path, force)?; } Ok(()) @@ -345,7 +347,7 @@ fn format( let doc = doc!(cx.editor); if let Some(format) = doc.format() { - let callback = make_format_callback(doc.id(), doc.version(), format); + let callback = make_format_callback(doc.id(), doc.version(), format, None); cx.jobs.callback(callback); } @@ -592,16 +594,17 @@ fn write_all_impl( let fmt = if auto_format { doc.auto_format().map(|fmt| { - let shared = fmt.shared(); - let callback = make_format_callback(doc.id(), doc.version(), shared.clone()); + let callback = + make_format_callback(doc.id(), doc.version(), fmt, Some((None, force))); jobs.callback(callback); - shared }) } else { None }; - doc.format_and_save::<_, PathBuf>(fmt, None, force)?; + if fmt.is_none() { + doc.save::(None, force)?; + } } if quit { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 1743fac2b3b9c..fe081442830e9 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -14,6 +14,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + use tokio::sync::Mutex; use helix_core::{ From aaa145067833c41686b7cdde9fb999018735bc04 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 11 Jul 2022 23:38:26 -0400 Subject: [PATCH 045/151] fix write-quit with auto format write-quit will now save all files successfully even when there is auto formatting --- helix-term/src/application.rs | 6 +++- helix-term/src/commands.rs | 19 +++++++------ helix-term/src/commands/dap.rs | 25 +++++++++-------- helix-term/src/commands/typed.rs | 17 +++++++----- helix-term/src/job.rs | 47 ++++++++++++++++++++++++++++---- helix-term/src/ui/editor.rs | 7 +++-- 6 files changed, 85 insertions(+), 36 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index e84739cd848a4..84eba22a74b55 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -971,7 +971,11 @@ impl Application { // want to try to run as much cleanup as we can, regardless of // errors along the way - let mut result = match self.jobs.finish().await { + let mut result = match self + .jobs + .finish(Some(&mut self.editor), Some(&mut self.compositor)) + .await + { Ok(_) => Ok(()), Err(err) => { log::error!("Error executing job: {}", err); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e76e0280c9d4c..afd9456414e31 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -47,6 +47,7 @@ use movement::Movement; use crate::{ args, compositor::{self, Component, Compositor}, + job::Callback, keymap::ReverseKeymap, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; @@ -107,10 +108,11 @@ impl<'a> Context<'a> { let callback = Box::pin(async move { let json = call.await?; let response = serde_json::from_value(json)?; - let call: job::Callback = - Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { callback(editor, compositor, response) - }); + }, + )); Ok(call) }); self.jobs.callback(callback); @@ -1925,8 +1927,8 @@ fn global_search(cx: &mut Context) { let show_picker = async move { let all_matches: Vec = UnboundedReceiverStream::new(all_matches_rx).collect().await; - let call: job::Callback = - Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { if all_matches.is_empty() { editor.set_status("No matches found"); return; @@ -1962,7 +1964,8 @@ fn global_search(cx: &mut Context) { }, ); compositor.push(Box::new(overlayed(picker))); - }); + }, + )); Ok(call) }; cx.jobs.callback(show_picker); @@ -2516,7 +2519,7 @@ async fn make_format_callback( write: Option<(Option, bool)>, ) -> anyhow::Result { let format = format.await?; - let call: job::Callback = Box::new(move |editor, _compositor| { + let call: job::Callback = Callback::EditorCompositor(Box::new(move |editor, _compositor| { if !editor.documents.contains_key(&doc_id) { return; } @@ -2546,7 +2549,7 @@ async fn make_format_callback( } else { log::info!("discarded formatting changes because the document changed"); } - }); + })); Ok(call) } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 12a3fbc74eb97..9c067ebae9562 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -118,11 +118,14 @@ fn dap_callback( let callback = Box::pin(async move { let json = call.await?; let response = serde_json::from_value(json)?; - let call: Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { - callback(editor, compositor, response) - }); + let call: Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + callback(editor, compositor, response) + }, + )); Ok(call) }); + jobs.callback(callback); } @@ -274,10 +277,10 @@ pub fn dap_launch(cx: &mut Context) { let completions = template.completion.clone(); let name = template.name.clone(); let callback = Box::pin(async move { - let call: Callback = Box::new(move |_editor, compositor| { + let call: Callback = Callback::Compositor(Box::new(move |compositor| { let prompt = debug_parameter_prompt(completions, name, Vec::new()); compositor.push(Box::new(prompt)); - }); + })); Ok(call) }); cx.jobs.callback(callback); @@ -332,10 +335,10 @@ fn debug_parameter_prompt( let config_name = config_name.clone(); let params = params.clone(); let callback = Box::pin(async move { - let call: Callback = Box::new(move |_editor, compositor| { + let call: Callback = Callback::Compositor(Box::new(move |compositor| { let prompt = debug_parameter_prompt(completions, config_name, params); compositor.push(Box::new(prompt)); - }); + })); Ok(call) }); cx.jobs.callback(callback); @@ -582,7 +585,7 @@ pub fn dap_edit_condition(cx: &mut Context) { None => return, }; let callback = Box::pin(async move { - let call: Callback = Box::new(move |editor, compositor| { + let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { let mut prompt = Prompt::new( "condition:".into(), None, @@ -610,7 +613,7 @@ pub fn dap_edit_condition(cx: &mut Context) { prompt.insert_str(&condition, editor) } compositor.push(Box::new(prompt)); - }); + })); Ok(call) }); cx.jobs.callback(callback); @@ -624,7 +627,7 @@ pub fn dap_edit_log(cx: &mut Context) { None => return, }; let callback = Box::pin(async move { - let call: Callback = Box::new(move |editor, compositor| { + let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { let mut prompt = Prompt::new( "log-message:".into(), None, @@ -651,7 +654,7 @@ pub fn dap_edit_log(cx: &mut Context) { prompt.insert_str(&log_message, editor); } compositor.push(Box::new(prompt)); - }); + })); Ok(call) }); cx.jobs.callback(callback); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 955b3b5887f53..a687b85494ca2 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -519,9 +519,10 @@ fn write_quit( write_impl(cx, args.first(), false)?; + tokio::task::block_in_place(|| helix_lsp::block_on(cx.jobs.finish(Some(cx.editor), None)))?; + let doc = doc_mut!(cx.editor); - tokio::task::block_in_place(|| helix_lsp::block_on(cx.jobs.finish()))?; tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) .map(|result| result.map(|_| ())) .unwrap_or(Ok(()))?; @@ -1491,12 +1492,13 @@ fn tree_sitter_subtree( let contents = format!("```tsq\n{}\n```", selected_node.to_sexp()); let callback = async move { - let call: job::Callback = - Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); let popup = Popup::new("hover", contents).auto_close(true); compositor.replace_or_push("hover", popup); - }); + }, + )); Ok(call) }; @@ -1604,8 +1606,8 @@ fn run_shell_command( if !output.is_empty() { let callback = async move { - let call: job::Callback = - Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { let contents = ui::Markdown::new( format!("```sh\n{}\n```", output), editor.syn_loader.clone(), @@ -1614,7 +1616,8 @@ fn run_shell_command( helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), )); compositor.replace_or_push("shell", popup); - }); + }, + )); Ok(call) }; diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index e51479925d418..a997653d9bbb5 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -5,7 +5,12 @@ use crate::compositor::Compositor; use futures_util::future::{BoxFuture, Future, FutureExt}; use futures_util::stream::{FuturesUnordered, StreamExt}; -pub type Callback = Box; +pub enum Callback { + EditorCompositor(Box), + Editor(Box), + Compositor(Box), +} + pub type JobFuture = BoxFuture<'static, anyhow::Result>>; pub struct Job { @@ -68,9 +73,11 @@ impl Jobs { ) { match call { Ok(None) => {} - Ok(Some(call)) => { - call(editor, compositor); - } + Ok(Some(call)) => match call { + Callback::EditorCompositor(call) => call(editor, compositor), + Callback::Editor(call) => call(editor), + Callback::Compositor(call) => call(compositor), + }, Err(e) => { editor.set_error(format!("Async job failed: {}", e)); } @@ -93,13 +100,41 @@ impl Jobs { } /// Blocks until all the jobs that need to be waited on are done. - pub async fn finish(&mut self) -> anyhow::Result<()> { + pub async fn finish( + &mut self, + mut editor: Option<&mut Editor>, + mut compositor: Option<&mut Compositor>, + ) -> anyhow::Result<()> { log::debug!("waiting on jobs..."); let mut wait_futures = std::mem::take(&mut self.wait_futures); while let (Some(job), tail) = wait_futures.into_future().await { match job { - Ok(_) => { + Ok(callback) => { wait_futures = tail; + + if let Some(callback) = callback { + // clippy doesn't realize this is an error without the derefs + #[allow(clippy::needless_option_as_deref)] + match callback { + Callback::EditorCompositor(call) + if editor.is_some() && compositor.is_some() => + { + call( + editor.as_deref_mut().unwrap(), + compositor.as_deref_mut().unwrap(), + ) + } + Callback::Editor(call) if editor.is_some() => { + call(editor.as_deref_mut().unwrap()) + } + Callback::Compositor(call) if compositor.is_some() => { + call(compositor.as_deref_mut().unwrap()) + } + + // skip callbacks for which we don't have the necessary references + _ => (), + } + } } Err(e) => { self.wait_futures = tail; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 3cd2130ad8713..abed7f9b4f839 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,7 +1,8 @@ use crate::{ commands, compositor::{Component, Context, Event, EventResult}, - job, key, + job::{self, Callback}, + key, keymap::{KeymapResult, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -944,9 +945,9 @@ impl EditorView { // TODO: Use an on_mode_change hook to remove signature help cxt.jobs.callback(async { - let call: job::Callback = Box::new(|_editor, compositor| { + let call: job::Callback = Callback::Compositor(Box::new(|compositor| { compositor.remove(SignatureHelp::ID); - }); + })); Ok(call) }); } From 8c667ef8deea1311a8e9569909ac11d79cc993ed Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 22 Jul 2022 01:07:31 -0400 Subject: [PATCH 046/151] factor editor event handling into function --- helix-term/src/application.rs | 101 +++++++++++++++++----------------- helix-term/src/ui/mod.rs | 6 +- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 84eba22a74b55..bae3f19293aa3 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -287,9 +287,6 @@ impl Application { where S: Stream> + Unpin, { - #[cfg(feature = "integration")] - let mut idle_handled = false; - loop { if self.editor.should_close() { return false; @@ -315,56 +312,19 @@ impl Application { self.render(); } event = self.editor.wait_event() => { - log::debug!("received editor event: {:?}", event); + let _idle_handled = self.handle_editor_event(event).await; - match event { - EditorEvent::DocumentSave(event) => { - self.handle_document_write(event); - self.render(); - } - EditorEvent::ConfigEvent(event) => { - self.handle_config_events(event); - self.render(); - } - EditorEvent::LanguageServerMessage((id, call)) => { - self.handle_language_server_message(call, id).await; - // limit render calls for fast language server messages - let last = self.editor.language_servers.incoming.is_empty(); - - if last || self.last_render.elapsed() > LSP_DEADLINE { - self.render(); - self.last_render = Instant::now(); - } - } - EditorEvent::DebuggerEvent(payload) => { - let needs_render = self.editor.handle_debugger_message(payload).await; - if needs_render { - self.render(); - } - } - EditorEvent::IdleTimer => { - self.editor.clear_idle_timer(); - self.handle_idle_timeout(); - - #[cfg(feature = "integration")] - { - log::debug!("idle handled"); - idle_handled = true; - } + // for integration tests only, reset the idle timer after every + // event to signal when test events are done processing + #[cfg(feature = "integration")] + { + if _idle_handled { + return true; } - } - } - } - // for integration tests only, reset the idle timer after every - // event to signal when test events are done processing - #[cfg(feature = "integration")] - { - if idle_handled { - return true; + self.editor.reset_idle_timer(); + } } - - self.editor.reset_idle_timer(); } } } @@ -517,6 +477,49 @@ impl Application { } } + #[inline(always)] + pub async fn handle_editor_event(&mut self, event: EditorEvent) -> bool { + log::debug!("received editor event: {:?}", event); + + match event { + EditorEvent::DocumentSave(event) => { + self.handle_document_write(event); + self.render(); + } + EditorEvent::ConfigEvent(event) => { + self.handle_config_events(event); + self.render(); + } + EditorEvent::LanguageServerMessage((id, call)) => { + self.handle_language_server_message(call, id).await; + // limit render calls for fast language server messages + let last = self.editor.language_servers.incoming.is_empty(); + + if last || self.last_render.elapsed() > LSP_DEADLINE { + self.render(); + self.last_render = Instant::now(); + } + } + EditorEvent::DebuggerEvent(payload) => { + let needs_render = self.editor.handle_debugger_message(payload).await; + if needs_render { + self.render(); + } + } + EditorEvent::IdleTimer => { + self.editor.clear_idle_timer(); + self.handle_idle_timeout(); + + #[cfg(feature = "integration")] + { + return true; + } + } + } + + false + } + pub fn handle_terminal_events(&mut self, event: Result) { let mut cx = crate::compositor::Context { editor: &mut self.editor, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6ac4dbb78e5da..f99dea0b8dc3c 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -14,7 +14,7 @@ mod statusline; mod text; use crate::compositor::{Component, Compositor}; -use crate::job; +use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; @@ -121,7 +121,7 @@ pub fn regex_prompt( if event == PromptEvent::Validate { let callback = async move { - let call: job::Callback = Box::new( + let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { let contents = Text::new(format!("{}", err)); let size = compositor.size(); @@ -135,7 +135,7 @@ pub fn regex_prompt( compositor.replace_or_push("invalid-regex", popup); }, - ); + )); Ok(call) }; From faa00d4cc3de89a89679f6315c0345182929beb2 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 7 Aug 2022 00:00:15 -0400 Subject: [PATCH 047/151] increase LSP shutdown timeout The Clang LAP takes a long time to shut down on Windows --- helix-view/src/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 58fcf238fe547..27b4458fdb4e4 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1288,14 +1288,14 @@ impl Editor { } } - /// Closes language servers with timeout. The default timeout is 500 ms, use + /// Closes language servers with timeout. The default timeout is 10000 ms, use /// `timeout` parameter to override this. pub async fn close_language_servers( &self, timeout: Option, ) -> Result<(), tokio::time::error::Elapsed> { tokio::time::timeout( - Duration::from_millis(timeout.unwrap_or(500)), + Duration::from_millis(timeout.unwrap_or(10000)), future::join_all( self.language_servers .iter_clients() From e5fd5e2a9c78a3f391cda24cb57d187e248f06d1 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 9 Aug 2022 00:32:04 -0400 Subject: [PATCH 048/151] fix panic when view of pending write is closed --- helix-term/src/commands/typed.rs | 18 ++++++++---------- helix-term/src/compositor.rs | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index a687b85494ca2..fa2ba5e6d40f1 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -34,6 +34,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> buffers_remaining_impl(cx.editor)? } + cx.block_try_flush_writes()?; cx.editor.close(view!(cx.editor).id); Ok(()) @@ -518,15 +519,7 @@ fn write_quit( } write_impl(cx, args.first(), false)?; - - tokio::task::block_in_place(|| helix_lsp::block_on(cx.jobs.finish(Some(cx.editor), None)))?; - - let doc = doc_mut!(cx.editor); - - tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) - .map(|result| result.map(|_| ())) - .unwrap_or(Ok(()))?; - + cx.block_try_flush_writes()?; quit(cx, &[], event) } @@ -540,6 +533,7 @@ fn force_write_quit( } write_impl(cx, args.first(), true)?; + cx.block_try_flush_writes()?; force_quit(cx, &[], event) } @@ -613,6 +607,8 @@ fn write_all_impl( buffers_remaining_impl(cx.editor)?; } + cx.block_try_flush_writes()?; + // close all views let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { @@ -682,6 +678,7 @@ fn quit_all( return Ok(()); } + cx.block_try_flush_writes()?; quit_all_impl(cx.editor, false) } @@ -710,8 +707,9 @@ fn cquit( .first() .and_then(|code| code.parse::().ok()) .unwrap_or(1); - cx.editor.exit_code = exit_code; + cx.editor.exit_code = exit_code; + cx.block_try_flush_writes()?; quit_all_impl(cx.editor, false) } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index c0898dae2ce00..6ef77341dcc6c 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -27,6 +27,24 @@ pub struct Context<'a> { pub jobs: &'a mut Jobs, } +impl<'a> Context<'a> { + /// Waits on all pending jobs, and then tries to flush all pending write + /// operations for the current document. + pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { + tokio::task::block_in_place(|| { + helix_lsp::block_on(self.jobs.finish(Some(self.editor), None)) + })?; + + let doc = doc_mut!(self.editor); + + tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) + .map(|result| result.map(|_| ())) + .unwrap_or(Ok(()))?; + + Ok(()) + } +} + pub trait Component: Any + AnyComponent { /// Process input events, return true if handled. fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult { From d544376590e9e9d649cc7406282b5ebd54dc6f0a Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 9 Aug 2022 23:32:01 -0400 Subject: [PATCH 049/151] reset idle timer for all events --- helix-term/src/application.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index bae3f19293aa3..70eae18a335bc 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -314,18 +314,21 @@ impl Application { event = self.editor.wait_event() => { let _idle_handled = self.handle_editor_event(event).await; - // for integration tests only, reset the idle timer after every - // event to signal when test events are done processing #[cfg(feature = "integration")] { if _idle_handled { return true; } - - self.editor.reset_idle_timer(); } } } + + // for integration tests only, reset the idle timer after every + // event to signal when test events are done processing + #[cfg(feature = "integration")] + { + self.editor.reset_idle_timer(); + } } } From 7b11e9ac6941188c6f6148961fdd97e34988490e Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 23 Aug 2022 17:21:26 -0400 Subject: [PATCH 050/151] fix erroneous write sender close This was not distinguishing the error types when trying a receive on an empty receiver, which was erroneously causing the sender to be closed when trying to flush the writes when there were none --- helix-view/src/document.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index fe081442830e9..86a1d6d208676 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -13,6 +13,7 @@ use std::future::Future; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; +use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::sync::Mutex; @@ -662,7 +663,16 @@ impl Document { let save_req = if block { rx.recv().await } else { - rx.try_recv().ok() + let msg = rx.try_recv(); + + if let Err(err) = msg { + match err { + TryRecvError::Empty => return None, + TryRecvError::Disconnected => None, + } + } else { + msg.ok() + } }; let save = match save_req { From 57de4e62519a59aece104867569c2b9ad044af54 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 24 Aug 2022 01:13:41 -0400 Subject: [PATCH 051/151] various fixes in write-all path --- helix-term/src/commands/typed.rs | 25 ++++-- helix-term/src/compositor.rs | 12 +-- helix-term/tests/integration.rs | 1 + helix-term/tests/test/commands.rs | 12 +-- helix-term/tests/test/helpers.rs | 28 ++++++- helix-term/tests/test/splits.rs | 122 ++++++++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 25 deletions(-) create mode 100644 helix-term/tests/test/splits.rs diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index fa2ba5e6d40f1..7fd619d92a0ec 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -573,13 +573,20 @@ fn write_all_impl( return Ok(()); } - let mut errors = String::new(); + let mut errors: Option = None; let auto_format = cx.editor.config().auto_format; let jobs = &mut cx.jobs; + // save all documents for doc in &mut cx.editor.documents.values_mut() { if doc.path().is_none() { - errors.push_str("cannot write a buffer without a filename\n"); + errors = errors + .or_else(|| Some(String::new())) + .map(|mut errs: String| { + errs.push_str("cannot write a buffer without a filename\n"); + errs + }); + continue; } @@ -591,7 +598,7 @@ fn write_all_impl( doc.auto_format().map(|fmt| { let callback = make_format_callback(doc.id(), doc.version(), fmt, Some((None, force))); - jobs.callback(callback); + jobs.add(Job::with_callback(callback).wait_before_exiting()); }) } else { None @@ -603,12 +610,12 @@ fn write_all_impl( } if quit { + cx.block_try_flush_writes()?; + if !force { buffers_remaining_impl(cx.editor)?; } - cx.block_try_flush_writes()?; - // close all views let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { @@ -616,7 +623,13 @@ fn write_all_impl( } } - bail!(errors) + if let Some(errs) = errors { + if !force { + bail!(errs); + } + } + + Ok(()) } fn write_all( diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 6ef77341dcc6c..5077807d97e5b 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -29,17 +29,17 @@ pub struct Context<'a> { impl<'a> Context<'a> { /// Waits on all pending jobs, and then tries to flush all pending write - /// operations for the current document. + /// operations for all documents. pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { tokio::task::block_in_place(|| { helix_lsp::block_on(self.jobs.finish(Some(self.editor), None)) })?; - let doc = doc_mut!(self.editor); - - tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) - .map(|result| result.map(|_| ())) - .unwrap_or(Ok(()))?; + for doc in &mut self.editor.documents.values_mut() { + tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) + .map(|result| result.map(|_| ())) + .unwrap_or(Ok(()))?; + } Ok(()) } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 8969e976e5f32..e3754c436190b 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -22,5 +22,6 @@ mod test { mod commands; mod movement; mod prompt; + mod splits; mod write; } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index b7c0f7cc4b383..0279e348c3a06 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -1,7 +1,4 @@ -use std::{ - io::{Read, Write}, - ops::RangeInclusive, -}; +use std::ops::RangeInclusive; use helix_core::diagnostic::Severity; use helix_term::application::Application; @@ -86,12 +83,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { ) .await?; - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - - let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!(RANGE.end().to_string(), file_content); + helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?; Ok(()) } diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index bbcc6632e40e6..ed1a03317e442 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -1,10 +1,15 @@ -use std::{io::Write, path::PathBuf, time::Duration}; +use std::{ + fs::File, + io::{Read, Write}, + path::PathBuf, + time::Duration, +}; use anyhow::bail; use crossterm::event::{Event, KeyEvent}; -use helix_core::{test, Selection, Transaction}; +use helix_core::{diagnostic::Severity, test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config}; -use helix_view::{doc, input::parse_macro}; +use helix_view::{doc, input::parse_macro, Editor}; use tempfile::NamedTempFile; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -213,3 +218,20 @@ pub fn app_with_file>(path: P) -> anyhow::Result { Config::default(), ) } + +pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> { + file.flush()?; + file.sync_all()?; + + let mut file_content = String::new(); + file.read_to_string(&mut file_content)?; + assert_eq!(content, file_content); + + Ok(()) +} + +pub fn assert_status_not_error(editor: &Editor) { + if let Some((_, sev)) = editor.get_status() { + assert_ne!(&Severity::Error, sev); + } +} diff --git a/helix-term/tests/test/splits.rs b/helix-term/tests/test/splits.rs new file mode 100644 index 0000000000000..70a517be4c1e7 --- /dev/null +++ b/helix-term/tests/test/splits.rs @@ -0,0 +1,122 @@ +use super::*; + +#[tokio::test(flavor = "multi_thread")] +async fn test_split_write_quit_all() -> anyhow::Result<()> { + let mut file1 = tempfile::NamedTempFile::new()?; + let mut file2 = tempfile::NamedTempFile::new()?; + let mut file3 = tempfile::NamedTempFile::new()?; + + test_key_sequences( + &mut helpers::app_with_file(file1.path())?, + vec![ + ( + Some(&format!( + "ihello1:sp:o {}ihello2:sp:o {}ihello3", + file2.path().to_string_lossy(), + file3.path().to_string_lossy() + )), + Some(&|app| { + let docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(3, docs.len()); + + let doc1 = docs + .iter() + .find(|doc| doc.path().unwrap() == file1.path()) + .unwrap(); + + assert_eq!("hello1", doc1.text().to_string()); + + let doc2 = docs + .iter() + .find(|doc| doc.path().unwrap() == file2.path()) + .unwrap(); + + assert_eq!("hello2", doc2.text().to_string()); + + let doc3 = docs + .iter() + .find(|doc| doc.path().unwrap() == file3.path()) + .unwrap(); + + assert_eq!("hello3", doc3.text().to_string()); + + helpers::assert_status_not_error(&app.editor); + assert_eq!(3, app.editor.tree.views().count()); + }), + ), + ( + Some(":wqa"), + Some(&|app| { + helpers::assert_status_not_error(&app.editor); + assert_eq!(0, app.editor.tree.views().count()); + }), + ), + ], + true, + ) + .await?; + + helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?; + helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?; + helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_split_write_quit_same_file() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + + test_key_sequences( + &mut helpers::app_with_file(file.path())?, + vec![ + ( + Some("Oihello:spogoodbye"), + Some(&|app| { + assert_eq!(2, app.editor.tree.views().count()); + helpers::assert_status_not_error(&app.editor); + + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + + assert_eq!( + helpers::platform_line("hello\ngoodbye"), + doc.text().to_string() + ); + + assert!(doc.is_modified()); + }), + ), + ( + Some(":wq"), + Some(&|app| { + helpers::assert_status_not_error(&app.editor); + assert_eq!(1, app.editor.tree.views().count()); + + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + + assert_eq!( + helpers::platform_line("hello\ngoodbye"), + doc.text().to_string() + ); + + assert!(!doc.is_modified()); + }), + ), + ], + false, + ) + .await?; + + helpers::assert_file_has_content( + file.as_file_mut(), + &helpers::platform_line("hello\ngoodbye"), + )?; + + Ok(()) +} From 6cffc7f05d0ca983690b46300cc82933389172c8 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 28 Aug 2022 23:23:00 -0400 Subject: [PATCH 052/151] Add note about log level for integration tests --- docs/CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 353cb4fd23f61..491cd4249b146 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,7 +35,8 @@ to `cargo install` anything either). Integration tests for helix-term can be run with `cargo integration-test`. Code contributors are strongly encouraged to write integration tests for their code. Existing tests can be used as examples. Helpers can be found in -[helpers.rs][helpers.rs] +[helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL` +environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`. ## Minimum Stable Rust Version (MSRV) Policy From f82a551b98cec165ac91aae15ba656a811229fde Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 30 Aug 2022 23:08:15 -0400 Subject: [PATCH 053/151] Rename doc save event names to past tense --- helix-term/src/application.rs | 6 +++--- helix-term/src/job.rs | 1 + helix-view/src/document.rs | 24 ++++++++++++------------ helix-view/src/editor.rs | 6 +++--- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 70eae18a335bc..a06460debc31d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -9,7 +9,7 @@ use helix_core::{ use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ align_view, - document::DocumentSaveEventResult, + document::DocumentSavedEventResult, editor::{ConfigEvent, EditorEvent}, theme, tree::Layout, @@ -431,7 +431,7 @@ impl Application { } } - pub fn handle_document_write(&mut self, doc_save_event: DocumentSaveEventResult) { + pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult) { if let Err(err) = doc_save_event { self.editor.set_error(err.to_string()); return; @@ -485,7 +485,7 @@ impl Application { log::debug!("received editor event: {:?}", event); match event { - EditorEvent::DocumentSave(event) => { + EditorEvent::DocumentSaved(event) => { self.handle_document_write(event); self.render(); } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index a997653d9bbb5..3c9e4511d46ba 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -107,6 +107,7 @@ impl Jobs { ) -> anyhow::Result<()> { log::debug!("waiting on jobs..."); let mut wait_futures = std::mem::take(&mut self.wait_futures); + while let (Some(job), tail) = wait_futures.into_future().await { match job { Ok(callback) => { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 86a1d6d208676..91d5f8aabafcb 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -89,14 +89,14 @@ impl Serialize for Mode { /// A snapshot of the text of a document that we want to write out to disk #[derive(Debug, Clone)] -pub struct DocumentSaveEvent { +pub struct DocumentSavedEvent { pub revision: usize, pub doc_id: DocumentId, pub path: PathBuf, } -pub type DocumentSaveEventResult = Result; -pub type DocumentSaveEventFuture = BoxFuture<'static, DocumentSaveEventResult>; +pub type DocumentSavedEventResult = Result; +pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>; pub struct Document { pub(crate) id: DocumentId, @@ -133,9 +133,9 @@ pub struct Document { last_saved_revision: usize, version: i32, // should be usize? pub(crate) modified_since_accessed: bool, - save_sender: Option>, - save_receiver: Option>, - current_save: Arc>>, + save_sender: Option>, + save_receiver: Option>, + current_save: Arc>>, diagnostics: Vec, language_server: Option>, @@ -616,7 +616,7 @@ impl Document { let mut file = File::create(&path).await?; to_writer(&mut file, encoding, &text).await?; - let event = DocumentSaveEvent { + let event = DocumentSavedEvent { revision: current_rev, doc_id, path, @@ -643,11 +643,11 @@ impl Document { .map_err(|err| anyhow!("failed to send save event: {}", err)) } - pub async fn await_save(&mut self) -> Option { + pub async fn await_save(&mut self) -> Option { self.await_save_impl(true).await } - async fn await_save_impl(&mut self, block: bool) -> Option { + async fn await_save_impl(&mut self, block: bool) -> Option { let mut current_save = self.current_save.lock().await; if let Some(ref mut save) = *current_save { log::trace!("reawaiting save of '{:?}'", self.path()); @@ -698,11 +698,11 @@ impl Document { /// Flushes the queue of pending writes. If any fail, /// it stops early before emptying the rest of the queue. - pub async fn try_flush_saves(&mut self) -> Option { + pub async fn try_flush_saves(&mut self) -> Option { self.flush_saves_impl(false).await } - async fn flush_saves_impl(&mut self, block: bool) -> Option { + async fn flush_saves_impl(&mut self, block: bool) -> Option { let mut final_result = None; while let Some(save_event) = self.await_save_impl(block).await { @@ -734,7 +734,7 @@ impl Document { /// Prepares the Document for being closed by stopping any new writes /// and flushing through the queue of pending writes. If any fail, /// it stops early before emptying the rest of the queue. - pub async fn close(&mut self) -> Option { + pub async fn close(&mut self) -> Option { if self.save_sender.is_some() { self.save_sender.take(); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 27b4458fdb4e4..fbd0b2b00c88c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,6 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, - document::{DocumentSaveEventResult, Mode}, + document::{DocumentSavedEventResult, Mode}, graphics::{CursorKind, Rect}, info::Info, input::KeyEvent, @@ -691,7 +691,7 @@ pub struct Editor { #[derive(Debug)] pub enum EditorEvent { - DocumentSave(DocumentSaveEventResult), + DocumentSaved(DocumentSavedEventResult), ConfigEvent(ConfigEvent), LanguageServerMessage((usize, Call)), DebuggerEvent(dap::Payload), @@ -1317,7 +1317,7 @@ impl Editor { biased; Some(Some(event)) = saves.next() => { - EditorEvent::DocumentSave(event) + EditorEvent::DocumentSaved(event) } Some(config_event) = self.config_events.1.recv() => { EditorEvent::ConfigEvent(config_event) From 18c32118b1df63895b662c1b37ada28ad0d5c9b5 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 30 Aug 2022 23:10:33 -0400 Subject: [PATCH 054/151] Save text in document saved events, use in status message --- helix-term/src/application.rs | 4 ++-- helix-view/src/document.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a06460debc31d..793993eee407f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -459,8 +459,8 @@ impl Application { doc.set_last_saved_revision(doc_save_event.revision); - let lines = doc.text().len_lines(); - let bytes = doc.text().len_bytes(); + let lines = doc_save_event.text.len_lines(); + let bytes = doc_save_event.text.len_bytes(); if let Err(err) = doc.set_path(Some(&doc_save_event.path)) { log::error!( diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 91d5f8aabafcb..61bea52725ff4 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -93,6 +93,7 @@ pub struct DocumentSavedEvent { pub revision: usize, pub doc_id: DocumentId, pub path: PathBuf, + pub text: Rope, } pub type DocumentSavedEventResult = Result; @@ -620,6 +621,7 @@ impl Document { revision: current_rev, doc_id, path, + text: text.clone(), }; if let Some(language_server) = language_server { From af03df3413f04ce7079d14388ce42fe70bd1397e Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 31 Aug 2022 15:08:00 -0400 Subject: [PATCH 055/151] fix write scratch buffer to file --- helix-term/src/commands/typed.rs | 4 -- helix-term/tests/test/write.rs | 71 ++++++++++++++++++++++++-------- helix-view/src/document.rs | 18 +++++--- 3 files changed, 67 insertions(+), 26 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 7fd619d92a0ec..d312c45f6ca9b 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -273,10 +273,6 @@ fn write_impl( let doc = doc_mut!(cx.editor); let path = path.map(AsRef::as_ref); - if doc.path().is_none() { - bail!("cannot write a buffer without a filename"); - } - let fmt = if editor_auto_fmt { doc.auto_format().map(|fmt| { let callback = make_format_callback( diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 544f1ba19896a..7d10543162e87 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -128,6 +128,52 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + + test_key_sequence( + &mut Application::new(Args::default(), Config::default())?, + Some(format!("ihello:w {}", file.path().to_string_lossy()).as_ref()), + Some(&|app| { + assert!(!app.editor.is_err()); + + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + assert_eq!(Some(&file.path().to_path_buf()), doc.path()); + }), + false, + ) + .await?; + + helpers::assert_file_has_content(file.as_file_mut(), &helpers::platform_line("hello"))?; + + Ok(()) +} + +#[tokio::test] +async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> { + helpers::test_key_sequence_with_input_text( + None, + ("#[\n|]#", "ihello:w", "hello#[\n|]#"), + &|app| { + assert!(app.editor.is_err()); + + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + assert_eq!(None, doc.path()); + }, + false, + ) + .await?; + + Ok(()) +} + #[tokio::test] async fn test_write_new_path() -> anyhow::Result<()> { let mut file1 = tempfile::NamedTempFile::new().unwrap(); @@ -164,24 +210,15 @@ async fn test_write_new_path() -> anyhow::Result<()> { ) .await?; - file1.as_file_mut().flush()?; - file1.as_file_mut().sync_all()?; - file2.as_file_mut().flush()?; - file2.as_file_mut().sync_all()?; + helpers::assert_file_has_content( + file1.as_file_mut(), + &helpers::platform_line("i can eat glass, it will not hurt me\n"), + )?; - let mut file1_content = String::new(); - file1.as_file_mut().read_to_string(&mut file1_content)?; - assert_eq!( - helpers::platform_line("i can eat glass, it will not hurt me\n"), - file1_content - ); - - let mut file2_content = String::new(); - file2.as_file_mut().read_to_string(&mut file2_content)?; - assert_eq!( - helpers::platform_line("i can eat glass, it will not hurt me\n"), - file2_content - ); + helpers::assert_file_has_content( + file2.as_file_mut(), + &helpers::platform_line("i can eat glass, it will not hurt me\n"), + )?; Ok(()) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 61bea52725ff4..ff64689e00139 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -574,7 +574,12 @@ impl Document { } }; - let identifier = self.identifier(); + let identifier = if self.path().is_some() { + Some(self.identifier()) + } else { + None + }; + let language_server = self.language_server.clone(); // mark changes up to now as saved @@ -628,10 +633,13 @@ impl Document { if !language_server.is_initialized() { return Ok(event); } - if let Some(notification) = - language_server.text_document_did_save(identifier, &text) - { - notification.await?; + + if let Some(identifier) = identifier { + if let Some(notification) = + language_server.text_document_did_save(identifier, &text) + { + notification.await?; + } } } From b3fc31a211293f48696d26855781577d1859c2c6 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 2 Sep 2022 09:20:28 -0400 Subject: [PATCH 056/151] move language server refresh to document saved event handler --- helix-term/src/application.rs | 42 ++++++++++++++++++++++------------- helix-term/src/commands.rs | 9 +------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 793993eee407f..60610c1d1f305 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -462,22 +462,34 @@ impl Application { let lines = doc_save_event.text.len_lines(); let bytes = doc_save_event.text.len_bytes(); - if let Err(err) = doc.set_path(Some(&doc_save_event.path)) { - log::error!( - "error setting path for doc '{:?}': {}", - doc.path(), - err.to_string(), - ); - self.editor.set_error(err.to_string()); - } else { - // TODO: fix being overwritten by lsp - self.editor.set_status(format!( - "'{}' written, {}L {}B", - get_relative_path(&doc_save_event.path).to_string_lossy(), - lines, - bytes - )); + if doc.path() != Some(&doc_save_event.path) { + if let Err(err) = doc.set_path(Some(&doc_save_event.path)) { + log::error!( + "error setting path for doc '{:?}': {}", + doc.path(), + err.to_string(), + ); + + self.editor.set_error(err.to_string()); + return; + } + + let loader = self.editor.syn_loader.clone(); + + // borrowing the same doc again to get around the borrow checker + let doc = self.editor.document_mut(doc_save_event.doc_id).unwrap(); + let id = doc.id(); + doc.detect_language(loader); + let _ = self.editor.refresh_language_server(id); } + + // TODO: fix being overwritten by lsp + self.editor.set_status(format!( + "'{}' written, {}L {}B", + get_relative_path(&doc_save_event.path).to_string_lossy(), + lines, + bytes + )); } #[inline(always)] diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index afd9456414e31..6deecbe26815c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2519,7 +2519,7 @@ async fn make_format_callback( write: Option<(Option, bool)>, ) -> anyhow::Result { let format = format.await?; - let call: job::Callback = Callback::EditorCompositor(Box::new(move |editor, _compositor| { + let call: job::Callback = Callback::Editor(Box::new(move |editor| { if !editor.documents.contains_key(&doc_id) { return; } @@ -2527,7 +2527,6 @@ async fn make_format_callback( let scrolloff = editor.config().scrolloff; let doc = doc_mut!(editor, &doc_id); let view = view_mut!(editor); - let loader = editor.syn_loader.clone(); if doc.version() == doc_version { apply_transaction(&format, doc, view); @@ -2536,14 +2535,8 @@ async fn make_format_callback( view.ensure_cursor_in_view(doc, scrolloff); if let Some((path, force)) = write { - let refresh_lang = path.is_some(); - if let Err(err) = doc.save(path, force) { editor.set_error(format!("Error saving: {}", err)); - } else if refresh_lang { - let id = doc.id(); - doc.detect_language(loader); - let _ = editor.refresh_language_server(id); } } } else { From b530a86d1f15cc7df0e1ae8aa4bd02109ac33a8f Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 2 Sep 2022 23:38:38 -0400 Subject: [PATCH 057/151] remove Callback::Compositor variant To reduce likelihood of accidental discarding of important callbacks --- helix-term/src/application.rs | 2 +- helix-term/src/commands/dap.rs | 18 ++++++++++-------- helix-term/src/compositor.rs | 4 +--- helix-term/src/job.rs | 20 ++++---------------- helix-term/src/ui/editor.rs | 7 ++++--- 5 files changed, 20 insertions(+), 31 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 60610c1d1f305..fe53d73d7f7d1 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -991,7 +991,7 @@ impl Application { let mut result = match self .jobs - .finish(Some(&mut self.editor), Some(&mut self.compositor)) + .finish(&mut self.editor, Some(&mut self.compositor)) .await { Ok(_) => Ok(()), diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 9c067ebae9562..c27417e397b14 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -277,10 +277,11 @@ pub fn dap_launch(cx: &mut Context) { let completions = template.completion.clone(); let name = template.name.clone(); let callback = Box::pin(async move { - let call: Callback = Callback::Compositor(Box::new(move |compositor| { - let prompt = debug_parameter_prompt(completions, name, Vec::new()); - compositor.push(Box::new(prompt)); - })); + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let prompt = debug_parameter_prompt(completions, name, Vec::new()); + compositor.push(Box::new(prompt)); + })); Ok(call) }); cx.jobs.callback(callback); @@ -335,10 +336,11 @@ fn debug_parameter_prompt( let config_name = config_name.clone(); let params = params.clone(); let callback = Box::pin(async move { - let call: Callback = Callback::Compositor(Box::new(move |compositor| { - let prompt = debug_parameter_prompt(completions, config_name, params); - compositor.push(Box::new(prompt)); - })); + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let prompt = debug_parameter_prompt(completions, config_name, params); + compositor.push(Box::new(prompt)); + })); Ok(call) }); cx.jobs.callback(callback); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 5077807d97e5b..35b9d05428290 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -31,9 +31,7 @@ impl<'a> Context<'a> { /// Waits on all pending jobs, and then tries to flush all pending write /// operations for all documents. pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { - tokio::task::block_in_place(|| { - helix_lsp::block_on(self.jobs.finish(Some(self.editor), None)) - })?; + tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?; for doc in &mut self.editor.documents.values_mut() { tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 3c9e4511d46ba..2888b6eb1565b 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -8,7 +8,6 @@ use futures_util::stream::{FuturesUnordered, StreamExt}; pub enum Callback { EditorCompositor(Box), Editor(Box), - Compositor(Box), } pub type JobFuture = BoxFuture<'static, anyhow::Result>>; @@ -76,7 +75,6 @@ impl Jobs { Ok(Some(call)) => match call { Callback::EditorCompositor(call) => call(editor, compositor), Callback::Editor(call) => call(editor), - Callback::Compositor(call) => call(compositor), }, Err(e) => { editor.set_error(format!("Async job failed: {}", e)); @@ -102,7 +100,7 @@ impl Jobs { /// Blocks until all the jobs that need to be waited on are done. pub async fn finish( &mut self, - mut editor: Option<&mut Editor>, + editor: &mut Editor, mut compositor: Option<&mut Compositor>, ) -> anyhow::Result<()> { log::debug!("waiting on jobs..."); @@ -117,20 +115,10 @@ impl Jobs { // clippy doesn't realize this is an error without the derefs #[allow(clippy::needless_option_as_deref)] match callback { - Callback::EditorCompositor(call) - if editor.is_some() && compositor.is_some() => - { - call( - editor.as_deref_mut().unwrap(), - compositor.as_deref_mut().unwrap(), - ) - } - Callback::Editor(call) if editor.is_some() => { - call(editor.as_deref_mut().unwrap()) - } - Callback::Compositor(call) if compositor.is_some() => { - call(compositor.as_deref_mut().unwrap()) + Callback::EditorCompositor(call) if compositor.is_some() => { + call(editor, compositor.as_deref_mut().unwrap()) } + Callback::Editor(call) => call(editor), // skip callbacks for which we don't have the necessary references _ => (), diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index abed7f9b4f839..73dfd52ca3db4 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -945,9 +945,10 @@ impl EditorView { // TODO: Use an on_mode_change hook to remove signature help cxt.jobs.callback(async { - let call: job::Callback = Callback::Compositor(Box::new(|compositor| { - compositor.remove(SignatureHelp::ID); - })); + let call: job::Callback = + Callback::EditorCompositor(Box::new(|_editor, compositor| { + compositor.remove(SignatureHelp::ID); + })); Ok(call) }); } From 3f07885b351748c5b8225aadb165f8ef7066f047 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 16 Sep 2022 23:17:48 -0400 Subject: [PATCH 058/151] document should save even if formatter fails --- helix-core/src/syntax.rs | 6 ++ helix-term/src/application.rs | 15 ++--- helix-term/src/commands.rs | 27 +++++---- helix-term/src/main.rs | 12 +++- helix-term/tests/test/auto_indent.rs | 1 + helix-term/tests/test/auto_pairs.rs | 1 + helix-term/tests/test/commands.rs | 14 +++-- helix-term/tests/test/helpers.rs | 90 ++++++++++++++++++++++++---- helix-term/tests/test/movement.rs | 4 +- helix-term/tests/test/prompt.rs | 4 +- helix-term/tests/test/splits.rs | 11 +++- helix-term/tests/test/write.rs | 66 +++++++++++++------- 12 files changed, 185 insertions(+), 66 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 61d382fdec6e3..f907629fe2f26 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -61,6 +61,12 @@ pub struct Configuration { pub language: Vec, } +impl Default for Configuration { + fn default() -> Self { + crate::config::default_syntax_loader() + } +} + // largely based on tree-sitter/cli/src/loader.rs #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index fe53d73d7f7d1..4fde2a66de450 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,7 +1,6 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; use helix_core::{ - config::{default_syntax_loader, user_syntax_loader}, diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, pos_at_coords, syntax, Selection, @@ -110,7 +109,11 @@ fn restore_term() -> Result<(), Error> { } impl Application { - pub fn new(args: Args, config: Config) -> Result { + pub fn new( + args: Args, + config: Config, + syn_loader_conf: syntax::Configuration, + ) -> Result { #[cfg(feature = "integration")] setup_integration_logging(); @@ -137,14 +140,6 @@ impl Application { }) .unwrap_or_else(|| theme_loader.default_theme(true_color)); - let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| { - eprintln!("Bad language config: {}", err); - eprintln!("Press to continue with default language config"); - use std::io::Read; - // This waits for an enter press. - let _ = std::io::stdin().read(&mut []); - default_syntax_loader() - }); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); let mut compositor = Compositor::new().context("build compositor")?; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6deecbe26815c..f6d583f51b666 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2518,7 +2518,8 @@ async fn make_format_callback( format: impl Future> + Send + 'static, write: Option<(Option, bool)>, ) -> anyhow::Result { - let format = format.await?; + let format = format.await; + let call: job::Callback = Callback::Editor(Box::new(move |editor| { if !editor.documents.contains_key(&doc_id) { return; @@ -2528,19 +2529,21 @@ async fn make_format_callback( let doc = doc_mut!(editor, &doc_id); let view = view_mut!(editor); - if doc.version() == doc_version { - apply_transaction(&format, doc, view); - doc.append_changes_to_history(view.id); - doc.detect_indent_and_line_ending(); - view.ensure_cursor_in_view(doc, scrolloff); + if let Ok(format) = format { + if doc.version() == doc_version { + apply_transaction(&format, doc, view); + doc.append_changes_to_history(view.id); + doc.detect_indent_and_line_ending(); + view.ensure_cursor_in_view(doc, scrolloff); + } else { + log::info!("discarded formatting changes because the document changed"); + } + } - if let Some((path, force)) = write { - if let Err(err) = doc.save(path, force) { - editor.set_error(format!("Error saving: {}", err)); - } + if let Some((path, force)) = write { + if let Err(err) = doc.save(path, force) { + editor.set_error(format!("Error saving: {}", err)); } - } else { - log::info!("discarded formatting changes because the document changed"); } })); diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 726bf9e3b6670..96b695c6fcde0 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -139,8 +139,18 @@ FLAGS: Err(err) => return Err(Error::new(err)), }; + let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { + eprintln!("Bad language config: {}", err); + eprintln!("Press to continue with default language config"); + use std::io::Read; + // This waits for an enter press. + let _ = std::io::stdin().read(&mut []); + helix_core::config::default_syntax_loader() + }); + // TODO: use the thread local executor to spawn the application task separately from the work pool - let mut app = Application::new(args, config).context("unable to create new application")?; + let mut app = Application::new(args, config, syn_loader_conf) + .context("unable to create new application")?; let exit_code = app.run(&mut EventStream::new()).await?; diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs index 2f638893c5deb..5c093a5db4548 100644 --- a/helix-term/tests/test/auto_indent.rs +++ b/helix-term/tests/test/auto_indent.rs @@ -8,6 +8,7 @@ async fn auto_indent_c() -> anyhow::Result<()> { ..Default::default() }, Config::default(), + helpers::test_syntax_conf(None), // switches to append mode? ( helpers::platform_line("void foo() {#[|}]#").as_ref(), diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs index ec47a5b4af4ff..caf80bd452015 100644 --- a/helix-term/tests/test/auto_pairs.rs +++ b/helix-term/tests/test/auto_pairs.rs @@ -13,6 +13,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { }, ..Default::default() }, + helpers::test_syntax_conf(None), ("#[\n|]#", "i(", "(#[|\n]#"), ) .await?; diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 0279e348c3a06..5238cc69b5f9d 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -1,16 +1,18 @@ use std::ops::RangeInclusive; use helix_core::diagnostic::Severity; -use helix_term::application::Application; use super::*; #[tokio::test(flavor = "multi_thread")] async fn test_write_quit_fail() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; test_key_sequence( - &mut helpers::app_with_file(file.path())?, + &mut app, Some("ihello:wq"), Some(&|app| { let mut docs: Vec<_> = app.editor.documents().collect(); @@ -30,7 +32,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_buffer_close_concurrent() -> anyhow::Result<()> { test_key_sequences( - &mut Application::new(Args::default(), Config::default())?, + &mut helpers::AppBuilder::new().build()?, vec![ ( None, @@ -70,8 +72,12 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { command.push_str(":bufferclose"); + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; + test_key_sequence( - &mut helpers::app_with_file(file.path())?, + &mut app, Some(&command), Some(&|app| { assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index ed1a03317e442..c2fbe9536171f 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -108,7 +108,7 @@ pub async fn test_key_sequence_with_input_text>( let test_case = test_case.into(); let mut app = match app { Some(app) => app, - None => Application::new(Args::default(), Config::default())?, + None => Application::new(Args::default(), Config::default(), test_syntax_conf(None))?, }; let (view, doc) = helix_view::current!(app.editor); @@ -132,16 +132,30 @@ pub async fn test_key_sequence_with_input_text>( .await } +/// Generates language configs that merge in overrides, like a user language +/// config. The argument string must be a raw TOML document. +pub fn test_syntax_conf(overrides: Option) -> helix_core::syntax::Configuration { + let mut lang = helix_loader::config::default_lang_config(); + + if let Some(overrides) = overrides { + let override_toml = toml::from_str(&overrides).unwrap(); + lang = helix_loader::merge_toml_values(lang, override_toml, 3); + } + + lang.try_into().unwrap() +} + /// Use this for very simple test cases where there is one input /// document, selection, and sequence of key presses, and you just /// want to verify the resulting document and selection. pub async fn test_with_config>( args: Args, config: Config, + syn_conf: helix_core::syntax::Configuration, test_case: T, ) -> anyhow::Result<()> { let test_case = test_case.into(); - let app = Application::new(args, config)?; + let app = Application::new(args, config, syn_conf)?; test_key_sequence_with_input_text( Some(app), @@ -162,7 +176,13 @@ pub async fn test_with_config>( } pub async fn test>(test_case: T) -> anyhow::Result<()> { - test_with_config(Args::default(), Config::default(), test_case).await + test_with_config( + Args::default(), + Config::default(), + test_syntax_conf(None), + test_case, + ) + .await } pub fn temp_file_with_contents>( @@ -207,16 +227,60 @@ pub fn new_readonly_tempfile() -> anyhow::Result { Ok(file) } -/// Creates a new Application with default config that opens the given file -/// path -pub fn app_with_file>(path: P) -> anyhow::Result { - Application::new( - Args { - files: vec![(path.into(), helix_core::Position::default())], - ..Default::default() - }, - Config::default(), - ) +#[derive(Default)] +pub struct AppBuilder { + args: Args, + config: Config, + syn_conf: helix_core::syntax::Configuration, + input: Option<(String, Selection)>, +} + +impl AppBuilder { + pub fn new() -> Self { + AppBuilder::default() + } + + pub fn with_file>( + mut self, + path: P, + pos: Option, + ) -> Self { + self.args.files.push((path.into(), pos.unwrap_or_default())); + self + } + + pub fn with_config(mut self, config: Config) -> Self { + self.config = config; + self + } + + pub fn with_input_text>(mut self, input_text: S) -> Self { + self.input = Some(test::print(&input_text.into())); + self + } + + pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self { + self.syn_conf = syn_conf; + self + } + + pub fn build(self) -> anyhow::Result { + let mut app = Application::new(self.args, self.config, self.syn_conf)?; + + if let Some((text, selection)) = self.input { + let (view, doc) = helix_view::current!(app.editor); + let sel = doc.selection(view.id).clone(); + let trans = Transaction::change_by_selection(doc.text(), &sel, |_| { + (0, doc.text().len_chars(), Some((text.clone()).into())) + }) + .with_selection(selection); + + // replace the initial text with the input text + doc.apply(&trans, view.id); + } + + Ok(app) + } } pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> { diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 45aae39e1cdcf..7212d026f3d73 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -70,7 +70,9 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { let file = helpers::temp_file_with_contents(content)?; - let mut app = helpers::app_with_file(file.path())?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; let (view, doc) = helix_view::current!(app.editor); let sel = doc.selection(view.id).clone(); diff --git a/helix-term/tests/test/prompt.rs b/helix-term/tests/test/prompt.rs index 2ab9604c6daec..62ec03f1ba61c 100644 --- a/helix-term/tests/test/prompt.rs +++ b/helix-term/tests/test/prompt.rs @@ -1,11 +1,9 @@ use super::*; -use helix_term::application::Application; - #[tokio::test] async fn test_history_completion() -> anyhow::Result<()> { test_key_sequence( - &mut Application::new(Args::default(), Config::default())?, + &mut AppBuilder::new().build()?, Some(":asdf:theme d"), Some(&|app| { assert!(!app.editor.is_err()); diff --git a/helix-term/tests/test/splits.rs b/helix-term/tests/test/splits.rs index 70a517be4c1e7..5807413a267e0 100644 --- a/helix-term/tests/test/splits.rs +++ b/helix-term/tests/test/splits.rs @@ -6,8 +6,12 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> { let mut file2 = tempfile::NamedTempFile::new()?; let mut file3 = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file1.path(), None) + .build()?; + test_key_sequences( - &mut helpers::app_with_file(file1.path())?, + &mut app, vec![ ( Some(&format!( @@ -66,9 +70,12 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_split_write_quit_same_file() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; test_key_sequences( - &mut helpers::app_with_file(file.path())?, + &mut app, vec![ ( Some("Oihello:spogoodbye"), diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 7d10543162e87..6aa51a31556bd 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -4,7 +4,6 @@ use std::{ }; use helix_core::diagnostic::Severity; -use helix_term::application::Application; use helix_view::doc; use super::*; @@ -12,9 +11,12 @@ use super::*; #[tokio::test] async fn test_write() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; test_key_sequence( - &mut helpers::app_with_file(file.path())?, + &mut app, Some("ithe gostak distims the doshes:w"), None, false, @@ -38,9 +40,12 @@ async fn test_write() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_write_quit() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; test_key_sequence( - &mut helpers::app_with_file(file.path())?, + &mut app, Some("ithe gostak distims the doshes:wq"), None, true, @@ -66,19 +71,16 @@ async fn test_write_concurrent() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); const RANGE: RangeInclusive = 1..=5000; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; for i in RANGE { let cmd = format!("%c{}:w", i); command.push_str(&cmd); } - test_key_sequence( - &mut helpers::app_with_file(file.path())?, - Some(&command), - None, - false, - ) - .await?; + test_key_sequence(&mut app, Some(&command), None, false).await?; file.as_file_mut().flush()?; file.as_file_mut().sync_all()?; @@ -93,9 +95,12 @@ async fn test_write_concurrent() -> anyhow::Result<()> { #[tokio::test] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; test_key_sequences( - &mut helpers::app_with_file(file.path())?, + &mut app, vec![ ( None, @@ -133,7 +138,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; test_key_sequence( - &mut Application::new(Args::default(), Config::default())?, + &mut AppBuilder::new().build()?, Some(format!("ihello:w {}", file.path().to_string_lossy()).as_ref()), Some(&|app| { assert!(!app.editor.is_err()); @@ -174,19 +179,40 @@ async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> { + let mut file = tempfile::Builder::new().suffix(".rs").tempfile()?; + + let lang_conf = indoc! {r#" + [[language]] + name = "rust" + formatter = { command = "bash", args = [ "-c", "exit 1" ] } + "#}; + + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .with_input_text("#[l|]#et foo = 0;\n") + .with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into()))) + .build()?; + + test_key_sequences(&mut app, vec![(Some(":w"), None)], false).await?; + + // file still saves + helpers::assert_file_has_content(file.as_file_mut(), "let foo = 0;\n")?; + + Ok(()) +} + #[tokio::test] async fn test_write_new_path() -> anyhow::Result<()> { let mut file1 = tempfile::NamedTempFile::new().unwrap(); let mut file2 = tempfile::NamedTempFile::new().unwrap(); + let mut app = helpers::AppBuilder::new() + .with_file(file1.path(), None) + .build()?; test_key_sequences( - &mut Application::new( - Args { - files: vec![(file1.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut app, vec![ ( Some("ii can eat glass, it will not hurt me:w"), @@ -228,7 +254,7 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; test_key_sequences( - &mut Application::new(Args::default(), Config::default())?, + &mut AppBuilder::new().build()?, vec![ ( None, From 9e64974f13e91470be7b411285b7a33c698da3aa Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 16 Sep 2022 23:32:25 -0400 Subject: [PATCH 059/151] remove Document::format_and_save --- helix-view/src/document.rs | 43 +++----------------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ff64689e00139..4a09bedc85ca3 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -520,32 +520,12 @@ impl Document { path: Option

, force: bool, ) -> Result<(), anyhow::Error> { - self.save_impl::, _>(None, path, force) - } - - pub fn format_and_save( - &mut self, - formatting: Option, - path: Option

, - force: bool, - ) -> anyhow::Result<()> - where - F: Future> + 'static + Send, - P: Into, - { - self.save_impl(formatting, path, force) + self.save_impl::, _>(path, force) } /// The `Document`'s text is encoded according to its encoding and written to the file located /// at its `path()`. - /// - /// If `formatting` is present, it supplies some changes that we apply to the text before saving. - fn save_impl( - &mut self, - formatting: Option, - path: Option

, - force: bool, - ) -> Result<(), anyhow::Error> + fn save_impl(&mut self, path: Option

, force: bool) -> Result<(), anyhow::Error> where F: Future> + 'static + Send, P: Into, @@ -561,7 +541,7 @@ impl Document { // we clone and move text + path into the future so that we asynchronously save the current // state without blocking any further edits. - let mut text = self.text().clone(); + let text = self.text().clone(); let path = match path { Some(path) => helix_core::path::get_canonicalized_path(&path.into())?, @@ -602,23 +582,6 @@ impl Document { } } - if let Some(fmt) = formatting { - match fmt.await { - Ok(transaction) => { - let success = transaction.changes().apply(&mut text); - if !success { - // This shouldn't happen, because the transaction changes were generated - // from the same text we're saving. - log::error!("failed to apply format changes before saving"); - } - } - Err(err) => { - // formatting failed: report error, and save file without modifications - log::error!("{}", err); - } - } - } - let mut file = File::create(&path).await?; to_writer(&mut file, encoding, &text).await?; From 31d1bbfddb112a1e38cf974793afc427a3614ecf Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 21 Sep 2022 18:34:56 -0400 Subject: [PATCH 060/151] review comments --- helix-term/src/application.rs | 35 ++++++++++++++++---------------- helix-term/src/commands/typed.rs | 2 +- helix-view/src/document.rs | 12 ++--------- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 4fde2a66de450..694c55c06b0f1 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -427,24 +427,25 @@ impl Application { } pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult) { - if let Err(err) = doc_save_event { - self.editor.set_error(err.to_string()); - return; - } - - let doc_save_event = doc_save_event.unwrap(); - let doc = self.editor.document_mut(doc_save_event.doc_id); - - if doc.is_none() { - warn!( - "received document saved event for non-existent doc id: {}", - doc_save_event.doc_id - ); + let doc_save_event = match doc_save_event { + Ok(event) => event, + Err(err) => { + self.editor.set_error(err.to_string()); + return; + } + }; - return; - } + let doc = match self.editor.document_mut(doc_save_event.doc_id) { + None => { + warn!( + "received document saved event for non-existent doc id: {}", + doc_save_event.doc_id + ); - let doc = doc.unwrap(); + return; + } + Some(doc) => doc, + }; debug!( "document {:?} saved with revision {}", @@ -472,7 +473,7 @@ impl Application { let loader = self.editor.syn_loader.clone(); // borrowing the same doc again to get around the borrow checker - let doc = self.editor.document_mut(doc_save_event.doc_id).unwrap(); + let doc = doc_mut!(self.editor, &doc_save_event.doc_id); let id = doc.id(); doc.detect_language(loader); let _ = self.editor.refresh_language_server(id); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index d312c45f6ca9b..070215cbfc027 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -21,7 +21,7 @@ pub struct TypableCommand { } fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { - log::info!("quitting..."); + log::debug!("quitting..."); if event != PromptEvent::Validate { return Ok(()); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 4a09bedc85ca3..0774e516b6cf0 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -554,12 +554,7 @@ impl Document { } }; - let identifier = if self.path().is_some() { - Some(self.identifier()) - } else { - None - }; - + let identifier = self.path().map(|_| self.identifier()); let language_server = self.language_server.clone(); // mark changes up to now as saved @@ -708,10 +703,7 @@ impl Document { /// and flushing through the queue of pending writes. If any fail, /// it stops early before emptying the rest of the queue. pub async fn close(&mut self) -> Option { - if self.save_sender.is_some() { - self.save_sender.take(); - } - + self.save_sender.take(); self.flush_saves_impl(true).await } From bf378e71b012b4c108d11825cee6eea7b69778c2 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 4 Oct 2022 01:37:02 -0400 Subject: [PATCH 061/151] fix tests --- helix-term/tests/test/movement.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 7212d026f3d73..81c66e534c55f 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -117,6 +117,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow:: ..Default::default() }, Config::default(), + helpers::test_syntax_conf(None), ( helpers::platform_line(indoc! {"\ #[/|]#// Increments @@ -148,6 +149,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res ..Default::default() }, Config::default(), + helpers::test_syntax_conf(None), ( helpers::platform_line(indoc! {"\ /// Increments @@ -180,6 +182,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any ..Default::default() }, Config::default(), + helpers::test_syntax_conf(None), ( helpers::platform_line(indoc! {"\ /// Increments @@ -210,6 +213,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any ..Default::default() }, Config::default(), + helpers::test_syntax_conf(None), ( helpers::platform_line(indoc! {"\ /// Increments From beb3427bfbaa88bec8b4c683e342f85eb53ad77d Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 4 Oct 2022 10:35:15 -0400 Subject: [PATCH 062/151] improve app close failure display --- helix-term/src/application.rs | 72 ++++++++++---------------------- helix-term/tests/test/helpers.rs | 12 +++++- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 694c55c06b0f1..2e49e6d11e036 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -951,28 +951,10 @@ impl Application { self.event_loop(input_stream).await; - // let mut save_errs = Vec::new(); - - // for doc in self.editor.documents_mut() { - // if let Some(Err(err)) = doc.close().await { - // save_errs.push(( - // doc.path() - // .map(|path| path.to_string_lossy().into_owned()) - // .unwrap_or_else(|| "".into()), - // err, - // )); - // } - // } - - let close_err = self.close().await.err(); + let close_errs = self.close().await; restore_term()?; - // for (path, err) in save_errs { - // self.editor.exit_code = 1; - // eprintln!("Error closing '{}': {}", path, err); - // } - - if let Some(err) = close_err { + for err in close_errs { self.editor.exit_code = 1; eprintln!("Error: {}", err); } @@ -980,49 +962,41 @@ impl Application { Ok(self.editor.exit_code) } - pub async fn close(&mut self) -> anyhow::Result<()> { + pub async fn close(&mut self) -> Vec { // [NOTE] we intentionally do not return early for errors because we // want to try to run as much cleanup as we can, regardless of // errors along the way + let mut errs = Vec::new(); - let mut result = match self + if let Err(err) = self .jobs .finish(&mut self.editor, Some(&mut self.compositor)) .await { - Ok(_) => Ok(()), - Err(err) => { - log::error!("Error executing job: {}", err); - Err(err) - } + log::error!("Error executing job: {}", err); + errs.push(err); }; for doc in self.editor.documents_mut() { - if let Some(save_result) = doc.close().await { - result = match save_result { - Ok(_) => result, - Err(err) => { - if let Some(path) = doc.path() { - log::error!( - "Error saving document '{}': {}", - path.to_string_lossy(), - err - ); - } - Err(err) - } - }; + if let Some(Err(err)) = doc.close().await { + if let Some(path) = doc.path() { + log::error!( + "Error saving document '{}': {}", + path.to_string_lossy(), + err + ); + } + errs.push(err); } } - match self.editor.close_language_servers(None).await { - Ok(_) => result, - Err(_) => { - log::error!("Timed out waiting for language servers to shutdown"); - Err(anyhow::format_err!( - "Timed out waiting for language servers to shutdown" - )) - } + if self.editor.close_language_servers(None).await.is_err() { + log::error!("Timed out waiting for language servers to shutdown"); + errs.push(anyhow::format_err!( + "Timed out waiting for language servers to shutdown" + )); } + + errs } } diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index c2fbe9536171f..5adc3354a6764 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -94,7 +94,17 @@ pub async fn test_key_sequences( tokio::time::timeout(TIMEOUT, event_loop).await?; } - app.close().await?; + let errs = app.close().await; + + if !errs.is_empty() { + log::error!("Errors closing app"); + + for err in errs { + log::error!("{}", err); + } + + bail!("Error closing app"); + } Ok(()) } From 30c93994b50888aaeb32c65c90426e997800ccea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 14 Oct 2022 16:22:21 +0900 Subject: [PATCH 063/151] Use a single save_queue on the editor --- helix-term/src/application.rs | 20 ++++- helix-term/src/commands.rs | 3 +- helix-term/src/commands/typed.rs | 88 +++++++++++-------- helix-term/src/compositor.rs | 22 +++-- helix-view/src/document.rs | 140 +++++-------------------------- helix-view/src/editor.rs | 79 ++++++++++------- 6 files changed, 160 insertions(+), 192 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2e49e6d11e036..6010e745cb79e 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,5 +1,5 @@ use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; +use futures_util::{Stream, StreamExt}; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -968,6 +968,24 @@ impl Application { // errors along the way let mut errs = Vec::new(); + // TODO: deduplicate with ctx.block_try_flush_writes + tokio::task::block_in_place(|| { + helix_lsp::block_on(async { + while let Some(save_event) = self.editor.save_queue.next().await { + match &save_event { + Ok(event) => { + let doc = doc_mut!(self.editor, &event.doc_id); + doc.set_last_saved_revision(event.revision); + } + Err(err) => { + log::error!("error saving document: {}", err); + } + }; + // TODO: if is_err: break? + } + }) + }); + if let Err(err) = self .jobs .finish(&mut self.editor, Some(&mut self.compositor)) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f6d583f51b666..87bbd6c6a98c6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2541,7 +2541,8 @@ async fn make_format_callback( } if let Some((path, force)) = write { - if let Err(err) = doc.save(path, force) { + let id = doc.id(); + if let Err(err) = editor.save(id, path, force) { editor.set_error(format!("Error saving: {}", err)); } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 070215cbfc027..ef7742568395b 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -79,12 +79,28 @@ fn buffer_close_by_ids_impl( doc_ids: &[DocumentId], force: bool, ) -> anyhow::Result<()> { + // TODO: deduplicate with ctx.block_try_flush_writes + tokio::task::block_in_place(|| { + helix_lsp::block_on(async { + while let Some(save_event) = editor.save_queue.next().await { + match &save_event { + Ok(event) => { + let doc = doc_mut!(editor, &event.doc_id); + doc.set_last_saved_revision(event.revision); + } + Err(err) => { + log::error!("error saving document: {}", err); + } + }; + // TODO: if is_err: break? + } + }) + }); + let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids .iter() .filter_map(|&doc_id| { - if let Err(CloseError::BufferModified(name)) = tokio::task::block_in_place(|| { - helix_lsp::block_on(editor.close_document(doc_id, force)) - }) { + if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) { Some((doc_id, name)) } else { None @@ -289,7 +305,8 @@ fn write_impl( }; if fmt.is_none() { - doc.save(path, force)?; + let id = doc.id(); + cx.editor.save(id, path, force)?; } Ok(()) @@ -569,40 +586,45 @@ fn write_all_impl( return Ok(()); } - let mut errors: Option = None; + let mut errors: Vec<&'static str> = Vec::new(); let auto_format = cx.editor.config().auto_format; let jobs = &mut cx.jobs; // save all documents - for doc in &mut cx.editor.documents.values_mut() { - if doc.path().is_none() { - errors = errors - .or_else(|| Some(String::new())) - .map(|mut errs: String| { - errs.push_str("cannot write a buffer without a filename\n"); - errs - }); - - continue; - } + let saves: Vec<_> = cx + .editor + .documents + .values() + .filter_map(|doc| { + if doc.path().is_none() { + errors.push("cannot write a buffer without a filename\n"); + return None; + } - if !doc.is_modified() { - continue; - } + if !doc.is_modified() { + return None; + } - let fmt = if auto_format { - doc.auto_format().map(|fmt| { - let callback = - make_format_callback(doc.id(), doc.version(), fmt, Some((None, force))); - jobs.add(Job::with_callback(callback).wait_before_exiting()); - }) - } else { + let fmt = if auto_format { + doc.auto_format().map(|fmt| { + let callback = + make_format_callback(doc.id(), doc.version(), fmt, Some((None, force))); + jobs.add(Job::with_callback(callback).wait_before_exiting()); + }) + } else { + None + }; + + if fmt.is_none() { + return Some(doc.id()); + } None - }; + }) + .collect(); - if fmt.is_none() { - doc.save::(None, force)?; - } + // manually call save for the rest of docs that don't have a formatter + for id in saves { + cx.editor.save::(id, None, force)?; } if quit { @@ -619,10 +641,8 @@ fn write_all_impl( } } - if let Some(errs) = errors { - if !force { - bail!(errs); - } + if !errors.is_empty() && !force { + bail!("{:?}", errors); } Ok(()) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 35b9d05428290..a4ffaff2591b5 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -1,3 +1,4 @@ +use futures_util::StreamExt; // Each component declares it's own size constraints and gets fitted based on it's parent. // Q: how does this work with popups? // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), ) @@ -33,11 +34,22 @@ impl<'a> Context<'a> { pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?; - for doc in &mut self.editor.documents.values_mut() { - tokio::task::block_in_place(|| helix_lsp::block_on(doc.try_flush_saves())) - .map(|result| result.map(|_| ())) - .unwrap_or(Ok(()))?; - } + tokio::task::block_in_place(|| { + helix_lsp::block_on(async { + while let Some(save_event) = self.editor.save_queue.next().await { + match &save_event { + Ok(event) => { + let doc = doc_mut!(self.editor, &event.doc_id); + doc.set_last_saved_revision(event.revision); + } + Err(err) => { + log::error!("error saving document: {}", err); + } + }; + // TODO: if is_err: break? + } + }) + }); Ok(()) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0774e516b6cf0..9fa1241ec451d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -13,10 +13,6 @@ use std::future::Future; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; -use tokio::sync::mpsc::error::TryRecvError; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; - -use tokio::sync::Mutex; use helix_core::{ encoding, @@ -134,9 +130,6 @@ pub struct Document { last_saved_revision: usize, version: i32, // should be usize? pub(crate) modified_since_accessed: bool, - save_sender: Option>, - save_receiver: Option>, - current_save: Arc>>, diagnostics: Vec, language_server: Option>, @@ -357,7 +350,6 @@ impl Document { let encoding = encoding.unwrap_or(encoding::UTF_8); let changes = ChangeSet::new(&text); let old_state = None; - let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel(); Self { id: DocumentId::default(), @@ -378,9 +370,6 @@ impl Document { savepoint: None, last_saved_revision: 0, modified_since_accessed: false, - save_sender: Some(save_sender), - save_receiver: Some(save_receiver), - current_save: Arc::new(Mutex::new(None)), language_server: None, } } @@ -519,21 +508,26 @@ impl Document { &mut self, path: Option

, force: bool, - ) -> Result<(), anyhow::Error> { - self.save_impl::, _>(path, force) + ) -> Result< + impl Future> + 'static + Send, + anyhow::Error, + > { + let path = path.map(|path| path.into()); + self.save_impl(path, force) + + // futures_util::future::Ready<_>, } /// The `Document`'s text is encoded according to its encoding and written to the file located /// at its `path()`. - fn save_impl(&mut self, path: Option

, force: bool) -> Result<(), anyhow::Error> - where - F: Future> + 'static + Send, - P: Into, - { - if self.save_sender.is_none() { - bail!("saves are closed for this document!"); - } - + fn save_impl( + &mut self, + path: Option, + force: bool, + ) -> Result< + impl Future> + 'static + Send, + anyhow::Error, + > { log::debug!( "submitting save of doc '{:?}'", self.path().map(|path| path.to_string_lossy()) @@ -544,7 +538,7 @@ impl Document { let text = self.text().clone(); let path = match path { - Some(path) => helix_core::path::get_canonicalized_path(&path.into())?, + Some(path) => helix_core::path::get_canonicalized_path(&path)?, None => { if self.path.is_none() { bail!("Can't save with no path set!"); @@ -564,7 +558,7 @@ impl Document { let encoding = self.encoding; // We encode the file according to the `Document`'s encoding. - let save_event = async move { + let future = async move { use tokio::fs::File; if let Some(parent) = path.parent() { // TODO: display a prompt asking the user if the directories should be created @@ -604,107 +598,15 @@ impl Document { Ok(event) }; - self.save_sender - .as_mut() - .unwrap() - .send(Box::pin(save_event)) - .map_err(|err| anyhow!("failed to send save event: {}", err)) - } - - pub async fn await_save(&mut self) -> Option { - self.await_save_impl(true).await - } - - async fn await_save_impl(&mut self, block: bool) -> Option { - let mut current_save = self.current_save.lock().await; - if let Some(ref mut save) = *current_save { - log::trace!("reawaiting save of '{:?}'", self.path()); - let result = save.await; - *current_save = None; - log::trace!("reawait save of '{:?}' result: {:?}", self.path(), result); - return Some(result); - } - - // return early if the receiver is closed - let rx = self.save_receiver.as_mut()?; - - let save_req = if block { - rx.recv().await - } else { - let msg = rx.try_recv(); - - if let Err(err) = msg { - match err { - TryRecvError::Empty => return None, - TryRecvError::Disconnected => None, - } - } else { - msg.ok() - } - }; - - let save = match save_req { - Some(save) => save, - None => { - self.save_receiver = None; - return None; - } - }; - - // save a handle to the future so that when a poll on this - // function gets cancelled, we don't lose it - *current_save = Some(save); - log::trace!("awaiting save of '{:?}'", self.path()); - - let result = (*current_save).as_mut().unwrap().await; - *current_save = None; - - log::trace!("save of '{:?}' result: {:?}", self.path(), result); - - Some(result) - } - - /// Flushes the queue of pending writes. If any fail, - /// it stops early before emptying the rest of the queue. - pub async fn try_flush_saves(&mut self) -> Option { - self.flush_saves_impl(false).await - } - - async fn flush_saves_impl(&mut self, block: bool) -> Option { - let mut final_result = None; - - while let Some(save_event) = self.await_save_impl(block).await { - let is_err = match &save_event { - Ok(event) => { - self.set_last_saved_revision(event.revision); - false - } - Err(err) => { - log::error!( - "error saving document {:?}: {}", - self.path().map(|path| path.to_string_lossy()), - err - ); - true - } - }; - - final_result = Some(save_event); - - if is_err { - break; - } - } - - final_result + Ok(future) } /// Prepares the Document for being closed by stopping any new writes /// and flushing through the queue of pending writes. If any fail, /// it stops early before emptying the rest of the queue. pub async fn close(&mut self) -> Option { - self.save_sender.take(); - self.flush_saves_impl(true).await + // TODO + None } /// Detect the programming language based on the file type. diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index fbd0b2b00c88c..c4789ee2baffb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,6 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, - document::{DocumentSavedEventResult, Mode}, + document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, graphics::{CursorKind, Rect}, info::Info, input::KeyEvent, @@ -9,7 +9,7 @@ use crate::{ Document, DocumentId, View, ViewId, }; -use futures_util::stream::{select_all::SelectAll, FuturesUnordered}; +use futures_util::stream::select_all::SelectAll; use futures_util::{future, StreamExt}; use helix_lsp::Call; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -29,7 +29,7 @@ use tokio::{ time::{sleep, Duration, Instant, Sleep}, }; -use anyhow::Error; +use anyhow::{anyhow, Error}; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; @@ -644,12 +644,20 @@ pub struct Breakpoint { pub log_message: Option, } +use futures_util::stream::{Flatten, Once}; + pub struct Editor { /// Current editing mode. pub mode: Mode, pub tree: Tree, pub next_document_id: DocumentId, pub documents: BTreeMap, + + // We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>. + // https://stackoverflow.com/a/66875668 + pub saves: HashMap>>, + pub save_queue: SelectAll>>>, + pub count: Option, pub selected_register: Option, pub registers: Registers, @@ -751,6 +759,8 @@ impl Editor { tree: Tree::new(area), next_document_id: DocumentId::default(), documents: BTreeMap::new(), + saves: HashMap::new(), + save_queue: SelectAll::new(), count: None, selected_register: None, macro_recording: None, @@ -1083,6 +1093,12 @@ impl Editor { self.new_document(doc) }; + let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel(); + self.saves.insert(id, save_sender); + + let stream = UnboundedReceiverStream::new(save_receiver).flatten(); + self.save_queue.push(stream); + self.switch(id, action); Ok(id) } @@ -1095,38 +1111,21 @@ impl Editor { self._refresh(); } - pub async fn close_document( - &mut self, - doc_id: DocumentId, - force: bool, - ) -> Result<(), CloseError> { + pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> { let doc = match self.documents.get_mut(&doc_id) { Some(doc) => doc, None => return Err(CloseError::DoesNotExist), }; - - // flush out any pending writes first to clear the modified status - if let Some(Err(err)) = doc.try_flush_saves().await { - return Err(CloseError::SaveError(err)); - } - if !force && doc.is_modified() { return Err(CloseError::BufferModified(doc.display_name().into_owned())); } - if let Some(Err(err)) = doc.close().await { - return Err(CloseError::SaveError(err)); - } + // This will also disallow any follow-up writes + self.saves.remove(&doc_id); - // Don't fail the whole write because the language server could not - // acknowledge the close if let Some(language_server) = doc.language_server() { - if let Err(err) = language_server - .text_document_did_close(doc.identifier()) - .await - { - log::error!("Error closing doc in language server: {}", err); - } + // TODO: track error + tokio::spawn(language_server.text_document_did_close(doc.identifier())); } enum Action { @@ -1188,6 +1187,28 @@ impl Editor { Ok(()) } + pub fn save>( + &mut self, + doc_id: DocumentId, + path: Option

, + force: bool, + ) -> anyhow::Result<()> { + // convert a channel of futures to pipe into main queue one by one + // via stream.then() ? then push into main future + + let path = path.map(|path| path.into()); + let doc = doc_mut!(self, &doc_id); + let future = doc.save(path, force)?; + // TODO: if no self.saves for that doc id then bail + // bail!("saves are closed for this document!"); + use futures_util::stream; + self.saves[&doc_id] + .send(stream::once(Box::pin(future))) + .map_err(|err| anyhow!("failed to send save event: {}", err))?; + + Ok(()) + } + pub fn resize(&mut self, area: Rect) { if self.tree.resize(area) { self._refresh(); @@ -1307,16 +1328,10 @@ impl Editor { } pub async fn wait_event(&mut self) -> EditorEvent { - let mut saves: FuturesUnordered<_> = self - .documents - .values_mut() - .map(Document::await_save) - .collect(); - tokio::select! { biased; - Some(Some(event)) = saves.next() => { + Some(event) = self.save_queue.next() => { EditorEvent::DocumentSaved(event) } Some(config_event) = self.config_events.1.recv() => { From b0212b36118f7a4d596510dabb0c011144c7af69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 16 Oct 2022 18:01:31 +0900 Subject: [PATCH 064/151] Deduplicate flush_writes --- helix-term/src/application.rs | 19 ++----------------- helix-term/src/commands/typed.rs | 17 +---------------- helix-term/src/compositor.rs | 18 +----------------- helix-view/src/editor.rs | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 50 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 6010e745cb79e..719683bfcb2a9 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,5 +1,5 @@ use arc_swap::{access::Map, ArcSwap}; -use futures_util::{Stream, StreamExt}; +use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -969,22 +969,7 @@ impl Application { let mut errs = Vec::new(); // TODO: deduplicate with ctx.block_try_flush_writes - tokio::task::block_in_place(|| { - helix_lsp::block_on(async { - while let Some(save_event) = self.editor.save_queue.next().await { - match &save_event { - Ok(event) => { - let doc = doc_mut!(self.editor, &event.doc_id); - doc.set_last_saved_revision(event.revision); - } - Err(err) => { - log::error!("error saving document: {}", err); - } - }; - // TODO: if is_err: break? - } - }) - }); + tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes())); if let Err(err) = self .jobs diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ef7742568395b..c18f3c393f525 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -80,22 +80,7 @@ fn buffer_close_by_ids_impl( force: bool, ) -> anyhow::Result<()> { // TODO: deduplicate with ctx.block_try_flush_writes - tokio::task::block_in_place(|| { - helix_lsp::block_on(async { - while let Some(save_event) = editor.save_queue.next().await { - match &save_event { - Ok(event) => { - let doc = doc_mut!(editor, &event.doc_id); - doc.set_last_saved_revision(event.revision); - } - Err(err) => { - log::error!("error saving document: {}", err); - } - }; - // TODO: if is_err: break? - } - }) - }); + tokio::task::block_in_place(|| helix_lsp::block_on(editor.flush_writes())); let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids .iter() diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index a4ffaff2591b5..7670378011776 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -1,4 +1,3 @@ -use futures_util::StreamExt; // Each component declares it's own size constraints and gets fitted based on it's parent. // Q: how does this work with popups? // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), ) @@ -34,22 +33,7 @@ impl<'a> Context<'a> { pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?; - tokio::task::block_in_place(|| { - helix_lsp::block_on(async { - while let Some(save_event) = self.editor.save_queue.next().await { - match &save_event { - Ok(event) => { - let doc = doc_mut!(self.editor, &event.doc_id); - doc.set_last_saved_revision(event.revision); - } - Err(err) => { - log::error!("error saving document: {}", err); - } - }; - // TODO: if is_err: break? - } - }) - }); + tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes())); Ok(()) } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c4789ee2baffb..c0efad05e3b7c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1348,4 +1348,19 @@ impl Editor { } } } + + pub async fn flush_writes(&mut self) { + while let Some(save_event) = self.save_queue.next().await { + match &save_event { + Ok(event) => { + let doc = doc_mut!(self, &event.doc_id); + doc.set_last_saved_revision(event.revision); + } + Err(err) => { + log::error!("error saving document: {}", err); + } + }; + // TODO: if is_err: break? + } + } } From b155e861adea1a29c5e4f9e717df8ff85a36052d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 16 Oct 2022 18:12:26 +0900 Subject: [PATCH 065/151] Use a write_count to determine how many writes left to flush --- helix-view/src/editor.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c0efad05e3b7c..ac0864e18e7d0 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -657,6 +657,7 @@ pub struct Editor { // https://stackoverflow.com/a/66875668 pub saves: HashMap>>, pub save_queue: SelectAll>>>, + pub write_count: usize, pub count: Option, pub selected_register: Option, @@ -761,6 +762,7 @@ impl Editor { documents: BTreeMap::new(), saves: HashMap::new(), save_queue: SelectAll::new(), + write_count: 0, count: None, selected_register: None, macro_recording: None, @@ -1206,6 +1208,8 @@ impl Editor { .send(stream::once(Box::pin(future))) .map_err(|err| anyhow!("failed to send save event: {}", err))?; + self.write_count += 1; + Ok(()) } @@ -1332,6 +1336,7 @@ impl Editor { biased; Some(event) = self.save_queue.next() => { + self.write_count -= 1; EditorEvent::DocumentSaved(event) } Some(config_event) = self.config_events.1.recv() => { @@ -1350,17 +1355,21 @@ impl Editor { } pub async fn flush_writes(&mut self) { - while let Some(save_event) = self.save_queue.next().await { - match &save_event { - Ok(event) => { - let doc = doc_mut!(self, &event.doc_id); - doc.set_last_saved_revision(event.revision); - } - Err(err) => { - log::error!("error saving document: {}", err); - } - }; - // TODO: if is_err: break? + while self.write_count > 0 { + if let Some(save_event) = self.save_queue.next().await { + match &save_event { + Ok(event) => { + let doc = doc_mut!(self, &event.doc_id); + doc.set_last_saved_revision(event.revision); + } + Err(err) => { + log::error!("error saving document: {}", err); + } + }; + // TODO: if is_err: break? + + self.write_count -= 1; + } } } } From 55b50d9e8368793d764d36c0f363f31252900b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 16 Oct 2022 23:39:58 +0900 Subject: [PATCH 066/151] Seems like this flush is unnecessary --- helix-term/src/application.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 719683bfcb2a9..2e49e6d11e036 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -968,9 +968,6 @@ impl Application { // errors along the way let mut errs = Vec::new(); - // TODO: deduplicate with ctx.block_try_flush_writes - tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes())); - if let Err(err) = self .jobs .finish(&mut self.editor, Some(&mut self.compositor)) From 1b6f7319cd605366de109f25821c6b84860f2a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 16 Oct 2022 23:44:36 +0900 Subject: [PATCH 067/151] Wire up save_queue as a part of new_document rather than open --- helix-view/src/editor.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ac0864e18e7d0..dc1a800e04cb9 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1062,6 +1062,13 @@ impl Editor { DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) }); doc.id = id; self.documents.insert(id, doc); + + let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel(); + self.saves.insert(id, save_sender); + + let stream = UnboundedReceiverStream::new(save_receiver).flatten(); + self.save_queue.push(stream); + id } @@ -1095,12 +1102,6 @@ impl Editor { self.new_document(doc) }; - let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel(); - self.saves.insert(id, save_sender); - - let stream = UnboundedReceiverStream::new(save_receiver).flatten(); - self.save_queue.push(stream); - self.switch(id, action); Ok(id) } From 2a43ee016453e39e2cc4cae510ce7d75606e7199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 16 Oct 2022 23:46:34 +0900 Subject: [PATCH 068/151] doc.close() now unused --- helix-view/src/document.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 9fa1241ec451d..78c6d032e9daf 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -601,14 +601,6 @@ impl Document { Ok(future) } - /// Prepares the Document for being closed by stopping any new writes - /// and flushing through the queue of pending writes. If any fail, - /// it stops early before emptying the rest of the queue. - pub async fn close(&mut self) -> Option { - // TODO - None - } - /// Detect the programming language based on the file type. pub fn detect_language(&mut self, config_loader: Arc) { if let Some(path) = &self.path { From 52ba550098745b463f59211a0172c334daef350b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 17 Oct 2022 00:02:14 +0900 Subject: [PATCH 069/151] Use flush_writes in application.close() --- helix-term/src/application.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2e49e6d11e036..6ca5b6572cf59 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -977,18 +977,7 @@ impl Application { errs.push(err); }; - for doc in self.editor.documents_mut() { - if let Some(Err(err)) = doc.close().await { - if let Some(path) = doc.path() { - log::error!( - "Error saving document '{}': {}", - path.to_string_lossy(), - err - ); - } - errs.push(err); - } - } + self.editor.flush_writes().await; if self.editor.close_language_servers(None).await.is_err() { log::error!("Timed out waiting for language servers to shutdown"); From e645804b0a8e42c9fb9ef8259a9b2ad47a57a6e6 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 16 Oct 2022 21:40:40 -0400 Subject: [PATCH 070/151] Editor::flush_writes returns an error --- helix-term/src/application.rs | 5 ++++- helix-term/src/commands/typed.rs | 23 +++++++++++------------ helix-term/src/compositor.rs | 4 +--- helix-view/src/editor.rs | 22 ++++++++++++---------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 6ca5b6572cf59..b4b4a6751bd02 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -977,7 +977,10 @@ impl Application { errs.push(err); }; - self.editor.flush_writes().await; + if let Err(err) = self.editor.flush_writes().await { + log::error!("Error writing: {}", err); + errs.push(err); + } if self.editor.close_language_servers(None).await.is_err() { log::error!("Timed out waiting for language servers to shutdown"); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index c18f3c393f525..eeeb46254ae05 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -75,17 +75,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> } fn buffer_close_by_ids_impl( - editor: &mut Editor, + cx: &mut compositor::Context, doc_ids: &[DocumentId], force: bool, ) -> anyhow::Result<()> { - // TODO: deduplicate with ctx.block_try_flush_writes - tokio::task::block_in_place(|| helix_lsp::block_on(editor.flush_writes())); + cx.block_try_flush_writes()?; let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids .iter() .filter_map(|&doc_id| { - if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) { + if let Err(CloseError::BufferModified(name)) = cx.editor.close_document(doc_id, force) { Some((doc_id, name)) } else { None @@ -94,11 +93,11 @@ fn buffer_close_by_ids_impl( .unzip(); if let Some(first) = modified_ids.first() { - let current = doc!(editor); + let current = doc!(cx.editor); // If the current document is unmodified, and there are modified // documents, switch focus to the first modified doc. if !modified_ids.contains(¤t.id()) { - editor.switch(*first, Action::Replace); + cx.editor.switch(*first, Action::Replace); } bail!( "{} unsaved buffer(s) remaining: {:?}", @@ -157,7 +156,7 @@ fn buffer_close( } let document_ids = buffer_gather_paths_impl(cx.editor, args); - buffer_close_by_ids_impl(cx.editor, &document_ids, false) + buffer_close_by_ids_impl(cx, &document_ids, false) } fn force_buffer_close( @@ -170,7 +169,7 @@ fn force_buffer_close( } let document_ids = buffer_gather_paths_impl(cx.editor, args); - buffer_close_by_ids_impl(cx.editor, &document_ids, true) + buffer_close_by_ids_impl(cx, &document_ids, true) } fn buffer_gather_others_impl(editor: &mut Editor) -> Vec { @@ -192,7 +191,7 @@ fn buffer_close_others( } let document_ids = buffer_gather_others_impl(cx.editor); - buffer_close_by_ids_impl(cx.editor, &document_ids, false) + buffer_close_by_ids_impl(cx, &document_ids, false) } fn force_buffer_close_others( @@ -205,7 +204,7 @@ fn force_buffer_close_others( } let document_ids = buffer_gather_others_impl(cx.editor); - buffer_close_by_ids_impl(cx.editor, &document_ids, true) + buffer_close_by_ids_impl(cx, &document_ids, true) } fn buffer_gather_all_impl(editor: &mut Editor) -> Vec { @@ -222,7 +221,7 @@ fn buffer_close_all( } let document_ids = buffer_gather_all_impl(cx.editor); - buffer_close_by_ids_impl(cx.editor, &document_ids, false) + buffer_close_by_ids_impl(cx, &document_ids, false) } fn force_buffer_close_all( @@ -235,7 +234,7 @@ fn force_buffer_close_all( } let document_ids = buffer_gather_all_impl(cx.editor); - buffer_close_by_ids_impl(cx.editor, &document_ids, true) + buffer_close_by_ids_impl(cx, &document_ids, true) } fn buffer_next( diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 7670378011776..971dc52d4ac28 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -32,9 +32,7 @@ impl<'a> Context<'a> { /// operations for all documents. pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { tokio::task::block_in_place(|| helix_lsp::block_on(self.jobs.finish(self.editor, None)))?; - - tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes())); - + tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?; Ok(()) } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index dc1a800e04cb9..9f74c5ec64f9a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -29,7 +29,7 @@ use tokio::{ time::{sleep, Duration, Instant, Sleep}, }; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, bail, Error}; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; @@ -1355,22 +1355,24 @@ impl Editor { } } - pub async fn flush_writes(&mut self) { + pub async fn flush_writes(&mut self) -> anyhow::Result<()> { while self.write_count > 0 { if let Some(save_event) = self.save_queue.next().await { - match &save_event { - Ok(event) => { - let doc = doc_mut!(self, &event.doc_id); - doc.set_last_saved_revision(event.revision); - } + self.write_count -= 1; + + let save_event = match save_event { + Ok(event) => event, Err(err) => { - log::error!("error saving document: {}", err); + self.set_error(err.to_string()); + bail!(err); } }; - // TODO: if is_err: break? - self.write_count -= 1; + let doc = doc_mut!(self, &save_event.doc_id); + doc.set_last_saved_revision(save_event.revision); } } + + Ok(()) } } From 759d55cc81da10c93f6cbfcbe605806b9dc37d2f Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 16 Oct 2022 22:41:01 -0400 Subject: [PATCH 071/151] fail if doc save sender is closed --- helix-view/src/editor.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9f74c5ec64f9a..ee84dbd3e1b2d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1202,10 +1202,12 @@ impl Editor { let path = path.map(|path| path.into()); let doc = doc_mut!(self, &doc_id); let future = doc.save(path, force)?; - // TODO: if no self.saves for that doc id then bail - // bail!("saves are closed for this document!"); + use futures_util::stream; - self.saves[&doc_id] + + self.saves + .get(&doc_id) + .ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))? .send(stream::once(Box::pin(future))) .map_err(|err| anyhow!("failed to send save event: {}", err))?; From 9a406b569b1b75c1970b81aac068110232ead2fd Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 18 Oct 2022 21:37:28 -0400 Subject: [PATCH 072/151] reduce LSP timeout to 3s --- helix-view/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ee84dbd3e1b2d..cd2b1ad45e775 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1323,7 +1323,7 @@ impl Editor { timeout: Option, ) -> Result<(), tokio::time::error::Elapsed> { tokio::time::timeout( - Duration::from_millis(timeout.unwrap_or(10000)), + Duration::from_millis(timeout.unwrap_or(3000)), future::join_all( self.language_servers .iter_clients() From 756253b43f5ec1d8cc6fce9e6ebcf3f9fee5bc5a Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 19 Oct 2022 00:01:00 -0400 Subject: [PATCH 073/151] fix tree_sitter_scopes --- helix-term/src/commands/typed.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index eeeb46254ae05..c1971d817f344 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1077,12 +1077,13 @@ fn tree_sitter_scopes( let contents = format!("```json\n{:?}\n````", scopes); let callback = async move { - let call: job::Callback = - Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); let popup = Popup::new("hover", contents).auto_close(true); compositor.replace_or_push("hover", popup); - }); + }, + )); Ok(call) }; From a7e7c2cc05d0998d8e45664cd3cb8b3ac46cca0e Mon Sep 17 00:00:00 2001 From: Sukera <11753998+Seelengrab@users.noreply.github.com> Date: Wed, 19 Oct 2022 14:53:22 +0200 Subject: [PATCH 074/151] Add `roots` for julia to languages.toml (#4361) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 3c324c78ab61d..39b81731fb799 100644 --- a/languages.toml +++ b/languages.toml @@ -591,7 +591,7 @@ name = "julia" scope = "source.julia" injection-regex = "julia" file-types = ["jl"] -roots = [] +roots = ["Manifest.toml", "Project.toml"] comment-token = "#" language-server = { command = "julia", args = [ "--startup-file=no", From 4174b25b3d0d39c09a6d4bcd7fa22ee98be52ed1 Mon Sep 17 00:00:00 2001 From: Fisher Darling Date: Thu, 20 Oct 2022 00:17:50 +0200 Subject: [PATCH 075/151] Pretty print `tree-sitter-subtree` expression (#4295) --- helix-core/src/syntax.rs | 108 +++++++++++++++++++++++++++++++ helix-term/src/commands/typed.rs | 4 +- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 61d382fdec6e3..b5083f55cacf9 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -2030,6 +2030,57 @@ impl> Iterator for Merge { } } +pub fn pretty_print_tree(fmt: &mut W, node: Node) -> fmt::Result { + pretty_print_tree_impl(fmt, node, true, None, 0) +} + +fn pretty_print_tree_impl( + fmt: &mut W, + node: Node, + is_root: bool, + field_name: Option<&str>, + depth: usize, +) -> fmt::Result { + fn is_visible(node: Node) -> bool { + node.is_missing() + || (node.is_named() && node.language().node_kind_is_visible(node.kind_id())) + } + + if is_visible(node) { + let indentation_columns = depth * 2; + write!(fmt, "{:indentation_columns$}", "")?; + + if let Some(field_name) = field_name { + write!(fmt, "{}: ", field_name)?; + } + + write!(fmt, "({}", node.kind())?; + } else if is_root { + write!(fmt, "(\"{}\")", node.kind())?; + } + + for child_idx in 0..node.child_count() { + if let Some(child) = node.child(child_idx) { + if is_visible(child) { + fmt.write_char('\n')?; + } + + pretty_print_tree_impl( + fmt, + child, + false, + node.field_name_for_child(child_idx as u32), + depth + 1, + )?; + } + } + + if is_visible(node) { + write!(fmt, ")")?; + } + + Ok(()) +} #[cfg(test)] mod test { use super::*; @@ -2201,6 +2252,63 @@ mod test { ); } + #[track_caller] + fn assert_pretty_print(source: &str, expected: &str, start: usize, end: usize) { + let source = Rope::from_str(source); + + let loader = Loader::new(Configuration { language: vec![] }); + let language = get_language("Rust").unwrap(); + + let config = HighlightConfiguration::new(language, "", "", "").unwrap(); + let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); + + let root = syntax + .tree() + .root_node() + .descendant_for_byte_range(start, end) + .unwrap(); + + let mut output = String::new(); + pretty_print_tree(&mut output, root).unwrap(); + + assert_eq!(expected, output); + } + + #[test] + fn test_pretty_print() { + let source = r#"/// Hello"#; + assert_pretty_print(source, "(line_comment)", 0, source.len()); + + // A large tree should be indented with fields: + let source = r#"fn main() { + println!("Hello, World!"); + }"#; + assert_pretty_print( + source, + concat!( + "(function_item\n", + " name: (identifier)\n", + " parameters: (parameters)\n", + " body: (block\n", + " (expression_statement\n", + " (macro_invocation\n", + " macro: (identifier)\n", + " (token_tree\n", + " (string_literal))))))", + ), + 0, + source.len(), + ); + + // Selecting a token should print just that token: + let source = r#"fn main() {}"#; + assert_pretty_print(source, r#"("fn")"#, 0, 1); + + // Error nodes are printed as errors: + let source = r#"}{"#; + assert_pretty_print(source, "(ERROR)", 0, source.len()); + } + #[test] fn test_load_runtime_file() { // Test to make sure we can load some data from the runtime directory. diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1bfc81536d354..13a0adcfa0a04 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1487,7 +1487,9 @@ fn tree_sitter_subtree( .root_node() .descendant_for_byte_range(from, to) { - let contents = format!("```tsq\n{}\n```", selected_node.to_sexp()); + let mut contents = String::from("```tsq\n"); + helix_core::syntax::pretty_print_tree(&mut contents, selected_node)?; + contents.push_str("\n```"); let callback = async move { let call: job::Callback = From 8c9bb23650ba3c0c0bc7b25a359f997e130feb25 Mon Sep 17 00:00:00 2001 From: Alex <3957610+CptPotato@users.noreply.github.com> Date: Thu, 20 Oct 2022 00:19:03 +0200 Subject: [PATCH 076/151] Update windows install instructions (#4351) --- README.md | 20 ++++++++++++++++++-- book/src/install.md | 43 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 85f80b6506cc4..5847a612e5050 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,25 @@ config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%App | OS | Command | | -------------------- | ------------------------------------------------ | -| Windows (cmd.exe) | `xcopy /e /i runtime %AppData%\helix\runtime` | +| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` | | Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` | -| Linux/macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` | +| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` | + +Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires +elevated priviliges - i.e. PowerShell or Cmd must be run as administrator. + +**PowerShell:** + +```powershell +New-Item -ItemType SymbolicLink -Target "runtime" -Path "$Env:AppData\helix\runtime" +``` + +**Cmd:** + +```cmd +cd %appdata%\helix +mklink /D runtime "\runtime" +``` This location can be overridden via the `HELIX_RUNTIME` environment variable. diff --git a/book/src/install.md b/book/src/install.md index 136e12c975cfb..4e7ea8dcc3ade 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -50,6 +50,23 @@ sudo dnf install helix sudo xbps-install helix ``` +## Windows + +Helix can be installed using [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/). + +**Scoop:** + +``` +scoop install helix +``` + +**Chocolatey:** + +``` +choco install helix +``` + + ## Build from source ``` @@ -64,11 +81,27 @@ Helix also needs its runtime files so make sure to copy/symlink the `runtime/` d config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden via the `HELIX_RUNTIME` environment variable. -| OS | command | -| ------------------- | ------------------------------------------------ | -| windows(cmd.exe) | `xcopy /e /i runtime %AppData%/helix/runtime` | -| windows(powershell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` | -| linux/macos | `ln -s $PWD/runtime ~/.config/helix/runtime` | +| OS | Command | +| -------------------- | ------------------------------------------------ | +| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` | +| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` | +| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` | + +Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires +elevated priviliges - i.e. PowerShell or Cmd must be run as administrator. + +**PowerShell:** + +```powershell +New-Item -ItemType SymbolicLink -Target "runtime" -Path "$Env:AppData\helix\runtime" +``` + +**Cmd:** + +```cmd +cd %appdata%\helix +mklink /D runtime "\runtime" +``` To use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder: From d801a6693c3d475b3942f705d3ef48d7966bdf65 Mon Sep 17 00:00:00 2001 From: midnightexigent <36641328+midnightexigent@users.noreply.github.com> Date: Thu, 20 Oct 2022 14:13:56 +0000 Subject: [PATCH 077/151] Allow using path suffixes to associate language file-types (#2455) * feat(syntax): add strategy to associate file to language through pattern File path will match if it ends with any of the file types provided in the config. Also used this feature to add support for the .git/config and .ssh/config files * Add /etc/ssh/ssh_config to languages.toml * cargo xtask docgen * Update languages.md * Update languages.md * Update book/src/languages.md Co-authored-by: Ivan Tham * Update book/src/languages.md Co-authored-by: Ivan Tham Co-authored-by: Ivan Tham --- book/src/languages.md | 2 +- helix-core/src/syntax.rs | 14 +++++++++++++- languages.toml | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/book/src/languages.md b/book/src/languages.md index 73c8121311e5b..9b90a2112b524 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -50,7 +50,7 @@ These configuration keys are available: | `name` | The name of the language | | `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.` or `text.` in case of markup languages | | `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | -| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. Extensions and full file names are supported. | +| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. This attempts to match by exact file name (`.zshrc`), then by file extension (`toml`), then by path suffix (`.git/config`). | | `shebangs` | The interpreters from the shebang line, for example `["sh", "bash"]` | | `roots` | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` | | `auto-format` | Whether to autoformat this language when saving | diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a08e508415a03..f9a2ea5f7630f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -471,9 +471,10 @@ impl Loader { for file_type in &config.file_types { // entry().or_insert(Vec::new).push(language_id); + let file_type = file_type.replace('/', &std::path::MAIN_SEPARATOR.to_string()); loader .language_config_ids_by_file_type - .insert(file_type.clone(), language_id); + .insert(file_type, language_id); } for shebang in &config.shebangs { loader @@ -498,6 +499,17 @@ impl Loader { path.extension() .and_then(|extension| extension.to_str()) .and_then(|extension| self.language_config_ids_by_file_type.get(extension)) + }) + .or_else(|| { + self.language_config_ids_by_file_type + .iter() + .find_map(|(file_type, id)| { + if path.to_str()?.ends_with(file_type) { + Some(id) + } else { + None + } + }) }); configuration_id.and_then(|&id| self.language_configs.get(id).cloned()) diff --git a/languages.toml b/languages.toml index 39b81731fb799..ce3ba20adc388 100644 --- a/languages.toml +++ b/languages.toml @@ -1054,7 +1054,7 @@ name = "git-config" scope = "source.gitconfig" roots = [] # TODO: allow specifying file-types as a regex so we can read directory names (e.g. `.git/config`) -file-types = [".gitmodules", ".gitconfig"] +file-types = [".gitmodules", ".gitconfig", ".git/config", ".config/git/config"] injection-regex = "git-config" comment-token = "#" indent = { tab-width = 4, unit = "\t" } From eee83620158bd710272c69194ac2ae2b51fdc708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 20 Oct 2022 22:50:54 +0200 Subject: [PATCH 078/151] fix(commands): no last picker error (#4387) --- helix-term/src/commands.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 87bbd6c6a98c6..db10d8518011b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2478,12 +2478,12 @@ pub fn command_palette(cx: &mut Context) { fn last_picker(cx: &mut Context) { // TODO: last picker does not seem to work well with buffer_picker - cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { + cx.callback = Some(Box::new(|compositor, cx| { if let Some(picker) = compositor.last_picker.take() { compositor.push(picker); + } else { + cx.editor.set_error("no last picker") } - // XXX: figure out how to show error when no last picker lifetime - // cx.editor.set_error("no last picker") })); } From de607830a2e9643dfe99828b9329f533a4f31fa9 Mon Sep 17 00:00:00 2001 From: Jared Ramirez Date: Thu, 20 Oct 2022 14:30:16 -0700 Subject: [PATCH 079/151] Upgrade rescript tree sitter & highlights (#4356) Co-authored-by: Michael Davis --- languages.toml | 2 +- runtime/queries/rescript/highlights.scm | 84 +++++++++++++++----- runtime/queries/rescript/injections.scm | 2 +- runtime/queries/rescript/locals.scm | 7 ++ runtime/queries/rescript/textobjects.scm | 98 +++++++++++++++++++++++- 5 files changed, 169 insertions(+), 24 deletions(-) create mode 100644 runtime/queries/rescript/locals.scm diff --git a/languages.toml b/languages.toml index ce3ba20adc388..51ffaef17d9d3 100644 --- a/languages.toml +++ b/languages.toml @@ -1140,7 +1140,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "rescript" -source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "4cd7ba91696886fdaca086fb32b5fd8cc294a129" } +source = { git = "https://github.com/jaredramirez/tree-sitter-rescript", rev = "65609807c628477f3b94052e7ef895885ac51c3c" } [[language]] name = "erlang" diff --git a/runtime/queries/rescript/highlights.scm b/runtime/queries/rescript/highlights.scm index a9cbeb5cd003d..ed41322b45a9d 100644 --- a/runtime/queries/rescript/highlights.scm +++ b/runtime/queries/rescript/highlights.scm @@ -10,20 +10,23 @@ [ (type_identifier) (unit_type) - "list" ] @type +(list ["list{" "}"] @type) +(list_pattern ["list{" "}"] @type) + [ (variant_identifier) (polyvar_identifier) -] @constant +] @constructor -(property_identifier) @variable.other.member +(record_type_field (property_identifier) @type) +(object_type (field (property_identifier) @type)) +(record_field (property_identifier) @variable.other.member) +(object (field (property_identifier) @variable.other.member)) +(member_expression (property_identifier) @variable.other.member) (module_identifier) @namespace -(jsx_identifier) @tag -(jsx_attribute (property_identifier) @variable.parameter) - ; Parameters ;---------------- @@ -42,8 +45,8 @@ "${" @punctuation.bracket "}" @punctuation.bracket) @embedded -(character) @constant.character -(escape_sequence) @constant.character.escape +(character) @string.special +(escape_sequence) @string.special ; Other literals ;--------------- @@ -60,11 +63,13 @@ ; Functions ;---------- +; parameter(s) in parens [ - (formal_parameters (value_identifier)) + (parameter (value_identifier)) (labeled_parameter (value_identifier)) ] @variable.parameter +; single parameter with no parens (function parameter: (value_identifier) @variable.parameter) ; Meta @@ -74,7 +79,7 @@ "@" "@@" (decorator_identifier) -] @label +] @keyword.directive (extension_identifier) @keyword ("%") @keyword @@ -82,13 +87,13 @@ ; Misc ;----- -(subscript_expression index: (string) @variable.other.member) +; (subscript_expression index: (string) @attribute) (polyvar_type_pattern "#" @constant) [ ("include") ("open") -] @keyword +] @keyword.control.import [ "as" @@ -96,25 +101,43 @@ "external" "let" "module" - "mutable" "private" "rec" "type" "and" -] @keyword + "assert" + "async" + "await" + "with" + "unpack" +] @keyword.storage.type + +"mutable" @keyword.storage.modifier [ "if" "else" "switch" -] @keyword + "when" +] @keyword.control.conditional [ "exception" "try" "catch" - "raise" -] @keyword +] @keyword.control.exception + +(call_expression + function: (value_identifier) @keyword.control.exception + (#eq? @keyword.control.exception "raise")) + +[ + "for" + "in" + "to" + "downto" + "while" +] @keyword.control.conditional [ "." @@ -129,17 +152,15 @@ "-" "-." "*" + "**" "*." - "/" "/." - "<" "<=" "==" "===" "!" "!=" "!==" - ">" ">=" "&&" "||" @@ -151,6 +172,10 @@ (uncurry) ] @operator +; Explicitly enclose these operators with binary_expression +; to avoid confusion with JSX tag delimiters +(binary_expression ["<" ">" "/"] @operator) + [ "(" ")" @@ -172,7 +197,24 @@ "~" "?" "=>" + ".." "..." -] @punctuation +] @punctuation.special (ternary_expression ["?" ":"] @operator) + +; JSX +;---------- +(jsx_identifier) @tag +(jsx_element + open_tag: (jsx_opening_element ["<" ">"] @punctuation.special)) +(jsx_element + close_tag: (jsx_closing_element ["<" "/" ">"] @punctuation.special)) +(jsx_self_closing_element ["/" ">" "<"] @punctuation.special) +(jsx_fragment [">" "<" "/"] @punctuation.special) +(jsx_attribute (property_identifier) @attribute) + +; Error +;---------- + +(ERROR) @keyword.control.exception diff --git a/runtime/queries/rescript/injections.scm b/runtime/queries/rescript/injections.scm index 201cce757bba8..03e29b0084af6 100644 --- a/runtime/queries/rescript/injections.scm +++ b/runtime/queries/rescript/injections.scm @@ -5,4 +5,4 @@ (#set! injection.language "javascript")) ((raw_gql) @injection.content - (#set! injection.language "graphql")) + (#set! injection.language "graphql")) \ No newline at end of file diff --git a/runtime/queries/rescript/locals.scm b/runtime/queries/rescript/locals.scm new file mode 100644 index 0000000000000..1240ed16010cd --- /dev/null +++ b/runtime/queries/rescript/locals.scm @@ -0,0 +1,7 @@ +(switch_expression) @local.scope +(if_expression) @local.scope + +; Definitions +;------------ +(type_declaration) @local.defintion +(let_binding) @local.defintion diff --git a/runtime/queries/rescript/textobjects.scm b/runtime/queries/rescript/textobjects.scm index fa1c4ff0d3c9a..4f1d8c53a81bb 100644 --- a/runtime/queries/rescript/textobjects.scm +++ b/runtime/queries/rescript/textobjects.scm @@ -3,14 +3,110 @@ (module_declaration definition: ((_) @class.inside)) @class.around +; Blocks +;------- + +(block (_) @function.inside) @function.around + ; Functions ;---------- (function body: (_) @function.inside) @function.around +; Calls +;------ + +(call_expression arguments: ((_) @parameter.inside)) @parameter.around + ; Comments ;--------- (comment) @comment.inside - (comment)+ @comment.around + +; Parameters +;----------- + +(function parameter: (_) @parameter.inside @parameter.around) + +(formal_parameters + "," + . (_) @parameter.inside + @parameter.around) +(formal_parameters + . (_) @parameter.inside + . ","? + @parameter.around) + +(arguments + "," @_arguments_start + . (_) @parameter.inside + @parameter.around) +(arguments + . (_) @parameter.inside + . ","? + @parameter.around) + +(function_type_parameters + "," + . (_) @parameter.inside + @parameter.around) +(function_type_parameters + . (_) @parameter.inside + . ","? + @parameter.around) + +(functor_parameters + "," + . (_) @parameter.inside + @parameter.around) +(functor_parameters + . (_) @parameter.inside + . ","? + @parameter.around) + +(type_parameters + "," + . (_) @parameter.inside + @parameter.around) +(type_parameters + . (_) @parameter.inside + . ","? + @parameter.around) + +(type_arguments + "," + . (_) @parameter.inside + @parameter.around) +(type_arguments + . (_) @parameter.inside + . ","? + @parameter.around) + +(decorator_arguments + "," + . (_) @parameter.inside + @parameter.around) +(decorator_arguments + . (_) @parameter.inside + . ","? + @parameter.around) + +(variant_parameters + "," + . (_) @parameter.inside + @parameter.around) +(variant_parameters + . (_) @parameter.inside + . ","? + @parameter.around) + +(polyvar_parameters + "," + . (_) @parameter.inside + @parameter.around) +(polyvar_parameters + . (_) @parameter.inside + . ","? + @parameter.around) + From 36f97b6aadec1a5c0cc61859d4f456f005983be0 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Thu, 20 Oct 2022 14:54:17 -0700 Subject: [PATCH 080/151] Add support for D (#4372) Co-authored-by: Michael Davis --- book/src/generated/lang-support.md | 1 + languages.toml | 15 ++ runtime/queries/d/highlights.scm | 231 +++++++++++++++++++++++++++++ runtime/queries/d/indents.scm | 17 +++ runtime/queries/d/injections.scm | 2 + runtime/queries/d/textobjects.scm | 9 ++ 6 files changed, 275 insertions(+) create mode 100644 runtime/queries/d/highlights.scm create mode 100644 runtime/queries/d/indents.scm create mode 100644 runtime/queries/d/injections.scm create mode 100644 runtime/queries/d/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 3fe9ef70e89fc..c6edc66d402be 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -15,6 +15,7 @@ | cpp | ✓ | ✓ | ✓ | `clangd` | | css | ✓ | | | `vscode-css-language-server` | | cue | ✓ | | | `cuelsp` | +| d | ✓ | ✓ | ✓ | `serve-d` | | dart | ✓ | | ✓ | `dart` | | devicetree | ✓ | | | | | diff | ✓ | | | | diff --git a/languages.toml b/languages.toml index 51ffaef17d9d3..5ad5c6e658494 100644 --- a/languages.toml +++ b/languages.toml @@ -1835,3 +1835,18 @@ roots = [] [[grammar]] name = "wast" source = { git = "https://github.com/wasm-lsp/tree-sitter-wasm", rev = "2ca28a9f9d709847bf7a3de0942a84e912f59088", subpath = "wast" } + +[[language]] +name = "d" +scope = "source.d" +file-types = [ "d", "dd" ] +roots = [] +comment-token = "//" +injection-regex = "d" +indent = { tab-width = 4, unit = " "} +language-server = { command = "serve-d" } +formatter = { command = "dfmt" } + +[[grammar]] +name = "d" +source = { git = "https://github.com/gdamore/tree-sitter-d", rev="601c4a1e8310fb2f3c43fa8a923d0d27497f3c04" } diff --git a/runtime/queries/d/highlights.scm b/runtime/queries/d/highlights.scm new file mode 100644 index 0000000000000..b5db8431634c6 --- /dev/null +++ b/runtime/queries/d/highlights.scm @@ -0,0 +1,231 @@ +; highlights.scm +; +; Highlighting queries for D code for use by Tree-Sitter. +; +; Copyright 2022 Garrett D'Amore +; +; Distributed under the MIT License. +; (See accompanying file LICENSE.txt or https://opensource.org/licenses/MIT) +; SPDX-License-Identifier: MIT + +; these are listed first, because they override keyword queries +(identity_expression (in) @operator) +(identity_expression (is) @operator) + +(storage_class) @keyword.storage + +(function_declaration (identifier) @function) + +(call_expression (identifier) @function) +(call_expression (type (identifier) @function)) + +(module_fqn) @namespace + +[ + (abstract) + (alias) + (align) + (asm) + (assert) + (auto) + (cast) + (const) + (debug) + (delete) + (deprecated) + (export) + (extern) + (final) + (immutable) + (in) + (inout) + (invariant) + (is) + (lazy) + ; "macro" - obsolete + (mixin) + (module) + (new) + (nothrow) + (out) + (override) + (package) + (pragma) + (private) + (protected) + (public) + (pure) + (ref) + (scope) + (shared) + (static) + (super) + (synchronized) + (template) + (this) + (throw) + (typeid) + (typeof) + (unittest) + (version) + (with) + (gshared) + (traits) + (vector) + (parameters_) +] @keyword + +[ + (class) + (struct) + (interface) + (union) + (enum) + (function) + (delegate) +] @keyword.storage.type + +[ + (break) + (case) + (catch) + (continue) + (do) + (default) + (finally) + (else) + (goto) + (if) + (switch) + (try) +] @keyword.control + +(return) @keyword.control.return + +(import) @keyword.control.import + +[ + (for) + (foreach) + (foreach_reverse) + (while) +] @keyword.control.repeat + +[ + (not_in) + (not_is) + "/=" + "/" + ".." + "..." + "&" + "&=" + "&&" + "|" + "|=" + "||" + "-" + "-=" + "--" + "+" + "+=" + "++" + "<" + "<=" + "<<" + "<<=" + ">" + ">=" + ">>=" + ">>>=" + ">>" + ">>>" + "!" + "!=" + "?" + "$" + "=" + "==" + "*" + "*=" + "%" + "%=" + "^" + "^=" + "^^" + "^^=" + "~" + "~=" + "@" + "=>" +] @operator + +[ + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + ";" + "." + ":" + "," +] @punctuation.delimiter + +[ + (true) + (false) +] @constant.builtin.boolean + +(null) @constant.builtin + +(special_keyword) @constant.builtin + +(directive) @keyword.directive +(shebang) @keyword.directive + +(comment) @comment + +[ + (void) + (bool) + (byte) + (ubyte) + (char) + (short) + (ushort) + (wchar) + (dchar) + (int) + (uint) + (long) + (ulong) + (real) + (double) +] @type.builtin + +[ + (cent) + (ucent) + (ireal) + (idouble) + (ifloat) + (creal) + (double) + (cfloat) +] @warning ; these types are deprecated + +(label (identifier) @label) +(goto_statement (goto) @keyword (identifier) @label) + +(string_literal) @string +(int_literal) @constant.numeric.integer +(float_literal) @constant.numeric.float +(char_literal) @constant.character +(identifier) @variable +(at_attribute) @attribute + +; everything after __EOF_ is plain text +(end_file) @ui.text diff --git a/runtime/queries/d/indents.scm b/runtime/queries/d/indents.scm new file mode 100644 index 0000000000000..73ff0b826eca9 --- /dev/null +++ b/runtime/queries/d/indents.scm @@ -0,0 +1,17 @@ +[ + (parameters) + (template_parameters) + (expression_statement) + (aggregate_body) + (function_body) + (scope_statement) + (block_statement) + (case_statement) +] @indent + +[ + (case) + (default) + "}" + "]" +] @outdent diff --git a/runtime/queries/d/injections.scm b/runtime/queries/d/injections.scm new file mode 100644 index 0000000000000..321c90add3710 --- /dev/null +++ b/runtime/queries/d/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/d/textobjects.scm b/runtime/queries/d/textobjects.scm new file mode 100644 index 0000000000000..9ca071605e14f --- /dev/null +++ b/runtime/queries/d/textobjects.scm @@ -0,0 +1,9 @@ +(function_declaration (function_body) @function.inside) @function.around +(comment) @comment.inside +(comment)+ @comment.around +(class_declaration (aggregate_body) @class.inside) @class.around +(interface_declaration (aggregate_body) @class.inside) @class.around +(struct_declaration (aggregate_body) @class.inside) @class.around +(unittest_declaration (block_statement) @test.insid) @test.around +(parameter) @parameter.inside +(template_parameter) @parameter.inside \ No newline at end of file From 4cff6250548d279ab69627177bd55312adb822a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 21 Oct 2022 00:05:04 +0200 Subject: [PATCH 081/151] chore(view): remove indent_unit helper fn (#4389) --- helix-term/src/commands.rs | 6 +++--- helix-view/src/document.rs | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index db10d8518011b..a8dafc33601b0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3022,7 +3022,7 @@ pub mod insert { // TODO: round out to nearest indentation level (for example a line with 3 spaces should // indent by one to reach 4 spaces). - let indent = Tendril::from(doc.indent_unit()); + let indent = Tendril::from(doc.indent_style.as_str()); let transaction = Transaction::insert( doc.text(), &doc.selection(view.id).clone().cursors(doc.text().slice(..)), @@ -3122,7 +3122,7 @@ pub mod insert { let count = cx.count(); let (view, doc) = current_ref!(cx.editor); let text = doc.text().slice(..); - let indent_unit = doc.indent_unit(); + let indent_unit = doc.indent_style.as_str(); let tab_size = doc.tab_width(); let auto_pairs = doc.auto_pairs(cx.editor); @@ -3647,7 +3647,7 @@ fn indent(cx: &mut Context) { let lines = get_lines(doc, view.id); // Indent by one level - let indent = Tendril::from(doc.indent_unit().repeat(count)); + let indent = Tendril::from(doc.indent_style.as_str().repeat(count)); let transaction = Transaction::change( doc.text(), diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 78c6d032e9daf..a723b89b59a3f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1052,14 +1052,6 @@ impl Document { .map_or(4, |config| config.tab_width) // fallback to 4 columns } - /// Returns a string containing a single level of indentation. - /// - /// TODO: we might not need this function anymore, since the information - /// is conveniently available in `Document::indent_style` now. - pub fn indent_unit(&self) -> &'static str { - self.indent_style.as_str() - } - pub fn changes(&self) -> &ChangeSet { &self.changes } From 1243db11a55a9f431a4b6bbf56c1953b2262d8b7 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 20 Oct 2022 13:36:21 -0500 Subject: [PATCH 082/151] Use helix_view::apply_transaction in integration-tests `helix_view::apply_transaction` closes over `Document::apply` and `View::apply` to ensure that jumplist entries are updated when a document changes from a transaction. `Document::apply` shouldn't be called directly - this helper function should be used instead. --- helix-term/tests/test/helpers.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 5adc3354a6764..a2d53e9d8644a 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -125,13 +125,12 @@ pub async fn test_key_sequence_with_input_text>( let sel = doc.selection(view.id).clone(); // replace the initial text with the input text - doc.apply( - &Transaction::change_by_selection(doc.text(), &sel, |_| { - (0, doc.text().len_chars(), Some((&test_case.in_text).into())) - }) - .with_selection(test_case.in_selection.clone()), - view.id, - ); + let transaction = Transaction::change_by_selection(doc.text(), &sel, |_| { + (0, doc.text().len_chars(), Some((&test_case.in_text).into())) + }) + .with_selection(test_case.in_selection.clone()); + + helix_view::apply_transaction(&transaction, doc, view); test_key_sequence( &mut app, @@ -286,7 +285,7 @@ impl AppBuilder { .with_selection(selection); // replace the initial text with the input text - doc.apply(&trans, view.id); + helix_view::apply_transaction(&trans, doc, view); } Ok(app) From 313579d27ce6ad55b5c2410856e6aa62a7778320 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 20 Oct 2022 14:14:27 -0500 Subject: [PATCH 083/151] Remove language-server configuration in integration tests This change removes language server configuration from the default languages.toml config for integration tests. No integration-tests currently depend on the availability of a language server but if any future test needs to, it may provide a language server configuration by passing an override into the `test_syntax_conf` helper. Language-servers in integration tests cause false-positive failures when running integration tests in GitHub Actions CI. The Windows runner appears to have `clangd` installed and all OS runners have the `R` binary installed but not the `R` language server package. If a test file created by `tempfile::NamedTempFile` happens to have a file extension of `r`, the test will most likely fail because the R language server will fail to start and will become a broken pipe, meaning that it will fail to shutdown within the timeout, causing a false-positive failure. This happens surprisingly often in practice. Language servers (especially rust-analyzer) also emit unnecessary log output when initializing, which this change silences. --- helix-term/tests/test/helpers.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index a2d53e9d8644a..e8bd6c35e0129 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -143,9 +143,27 @@ pub async fn test_key_sequence_with_input_text>( /// Generates language configs that merge in overrides, like a user language /// config. The argument string must be a raw TOML document. +/// +/// By default, language server configuration is dropped from the languages.toml +/// document. If a language-server is necessary for a test, it must be explicitly +/// added in `overrides`. pub fn test_syntax_conf(overrides: Option) -> helix_core::syntax::Configuration { let mut lang = helix_loader::config::default_lang_config(); + for lang_config in lang + .as_table_mut() + .expect("Expected languages.toml to be a table") + .get_mut("language") + .expect("Expected languages.toml to have \"language\" keys") + .as_array_mut() + .expect("Expected an array of language configurations") + { + lang_config + .as_table_mut() + .expect("Expected language config to be a TOML table") + .remove("language-server"); + } + if let Some(overrides) = overrides { let override_toml = toml::from_str(&overrides).unwrap(); lang = helix_loader::merge_toml_values(lang, override_toml, 3); @@ -236,7 +254,6 @@ pub fn new_readonly_tempfile() -> anyhow::Result { Ok(file) } -#[derive(Default)] pub struct AppBuilder { args: Args, config: Config, @@ -244,6 +261,17 @@ pub struct AppBuilder { input: Option<(String, Selection)>, } +impl Default for AppBuilder { + fn default() -> Self { + Self { + args: Args::default(), + config: Config::default(), + syn_conf: test_syntax_conf(None), + input: None, + } + } +} + impl AppBuilder { pub fn new() -> Self { AppBuilder::default() From 66238e855666f979306dc8698aeb0f35aaa84776 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 20 Oct 2022 14:27:29 -0500 Subject: [PATCH 084/151] Silence dead_code warning on AppBuilder::with_config This function is not currently used but is likely to be useful in the future, so this change silences the dead_code warning. --- helix-term/tests/test/helpers.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index e8bd6c35e0129..2c5043d68cf23 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -286,6 +286,8 @@ impl AppBuilder { self } + // Remove this attribute once `with_config` is used in a test: + #[allow(dead_code)] pub fn with_config(mut self, config: Config) -> Self { self.config = config; self From e25af1f7441fd1eccae580ba2e8e0eebc2be74f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 21 Oct 2022 01:53:05 +0200 Subject: [PATCH 085/151] feat(view): re-use align_view function (#4390) --- helix-view/src/editor.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cd2b1ad45e775..fe243b92d181f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,4 +1,5 @@ use crate::{ + align_view, clipboard::{get_clipboard_provider, ClipboardProvider}, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, graphics::{CursorKind, Rect}, @@ -6,7 +7,7 @@ use crate::{ input::KeyEvent, theme::{self, Theme}, tree::{self, Tree}, - Document, DocumentId, View, ViewId, + Align, Document, DocumentId, View, ViewId, }; use futures_util::stream::select_all::SelectAll; @@ -957,13 +958,7 @@ impl Editor { let doc = doc_mut!(self, &doc_id); doc.ensure_view_init(view.id); - // TODO: reuse align_view - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let line = doc.text().char_to_line(pos); - view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + align_view(doc, view, Align::Center); } pub fn switch(&mut self, id: DocumentId, action: Action) { From 6a0b450f55675c76d67bfb026caa2df4b601153b Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Thu, 20 Oct 2022 20:22:20 -0400 Subject: [PATCH 086/151] Fix multi byte auto pairs (#4024) * Fix test::print for Unicode The print function was not generating correct translations when the input has Unicode (non-ASCII) in it. This is due to its use of String::len, which gives the length in bytes, not chars. * Fix multi-code point auto pairs The current code for auto pairs is counting offsets by summing the length of the open and closing chars with char::len_utf8. Unfortunately, this gives back bytes, and the offset needs to be in chars. Additionally, it was discovered that there was a preexisting bug where the selection was not computed correctly in the case that the cursor was: 1. a single grapheme in width 2. this grapheme was more than one char 3. the direction of the cursor is backwards 4. a secondary range In this case, the offset was not being added into the anchor. This was fixed. * migrate auto pairs tests to integration * review comments --- helix-core/src/auto_pairs.rs | 594 +--------------------------- helix-core/src/test.rs | 127 +++++- helix-term/tests/test/auto_pairs.rs | 551 +++++++++++++++++++++++++- helix-view/src/clipboard.rs | 2 +- 4 files changed, 683 insertions(+), 591 deletions(-) diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index edc404ac5ccf8..072c93d014c0c 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -146,13 +146,7 @@ fn prev_char(doc: &Rope, pos: usize) -> Option { } /// calculate what the resulting range should be for an auto pair insertion -fn get_next_range( - doc: &Rope, - start_range: &Range, - offset: usize, - typed_char: char, - len_inserted: usize, -) -> Range { +fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range { // When the character under the cursor changes due to complete pair // insertion, we must look backward a grapheme and then add the length // of the insertion to put the resulting cursor in the right place, e.g. @@ -172,8 +166,8 @@ fn get_next_range( // inserting at the very end of the document after the last newline if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() { return Range::new( - start_range.anchor + offset + typed_char.len_utf8(), - start_range.head + offset + typed_char.len_utf8(), + start_range.anchor + offset + 1, + start_range.head + offset + 1, ); } @@ -203,21 +197,18 @@ fn get_next_range( // trivial case: only inserted a single-char opener, just move the selection if len_inserted == 1 { let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward { - start_range.anchor + offset + typed_char.len_utf8() + start_range.anchor + offset + 1 } else { start_range.anchor + offset }; - return Range::new( - end_anchor, - start_range.head + offset + typed_char.len_utf8(), - ); + return Range::new(end_anchor, start_range.head + offset + 1); } // If the head = 0, then we must be in insert mode with a backward // cursor, which implies the head will just move let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward { - start_range.head + offset + typed_char.len_utf8() + start_range.head + offset + 1 } else { // We must have a forward cursor, which means we must move to the // other end of the grapheme to get to where the new characters @@ -243,8 +234,7 @@ fn get_next_range( (_, Direction::Forward) => { if single_grapheme { - graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) - + typed_char.len_utf8() + graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) + 1 // if we are appending, the anchor stays where it is; only offset // for multiple range insertions @@ -258,7 +248,9 @@ fn get_next_range( // if we're backward, then the head is at the first char // of the typed char, so we need to add the length of // the closing char - graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted + graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + + len_inserted + + offset } else { // when we are inserting in front of a selection, we need to move // the anchor over by however many characters were inserted overall @@ -279,9 +271,12 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { let next_char = doc.get_char(cursor); let len_inserted; + // Since auto pairs are currently limited to single chars, we're either + // inserting exactly one or two chars. When arbitrary length pairs are + // added, these will need to be changed. let change = match next_char { Some(_) if !pair.should_close(doc, start_range) => { - len_inserted = pair.open.len_utf8(); + len_inserted = 1; let mut tendril = Tendril::new(); tendril.push(pair.open); (cursor, cursor, Some(tendril)) @@ -289,12 +284,12 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { _ => { // insert open & close let pair_str = Tendril::from_iter([pair.open, pair.close]); - len_inserted = pair.open.len_utf8() + pair.close.len_utf8(); + len_inserted = 2; (cursor, cursor, Some(pair_str)) } }; - let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted); + let next_range = get_next_range(doc, start_range, offs, len_inserted); end_ranges.push(next_range); offs += len_inserted; @@ -308,7 +303,6 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { let mut end_ranges = SmallVec::with_capacity(selection.len()); - let mut offs = 0; let transaction = Transaction::change_by_selection(doc, selection, |start_range| { @@ -320,13 +314,13 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { // return transaction that moves past close (cursor, cursor, None) // no-op } else { - len_inserted += pair.close.len_utf8(); + len_inserted = 1; let mut tendril = Tendril::new(); tendril.push(pair.close); (cursor, cursor, Some(tendril)) }; - let next_range = get_next_range(doc, start_range, offs, pair.close, len_inserted); + let next_range = get_next_range(doc, start_range, offs, len_inserted); end_ranges.push(next_range); offs += len_inserted; @@ -362,11 +356,11 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { pair_str.push(pair.close); } - len_inserted += pair_str.len(); + len_inserted += pair_str.chars().count(); (cursor, cursor, Some(pair_str)) }; - let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted); + let next_range = get_next_range(doc, start_range, offs, len_inserted); end_ranges.push(next_range); offs += len_inserted; @@ -377,551 +371,3 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { log::debug!("auto pair transaction: {:#?}", t); t } - -#[cfg(test)] -mod test { - use super::*; - use smallvec::smallvec; - - const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str(); - - fn differing_pairs() -> impl Iterator { - DEFAULT_PAIRS.iter().filter(|(open, close)| open != close) - } - - fn matching_pairs() -> impl Iterator { - DEFAULT_PAIRS.iter().filter(|(open, close)| open == close) - } - - fn test_hooks( - in_doc: &Rope, - in_sel: &Selection, - ch: char, - pairs: &[(char, char)], - expected_doc: &Rope, - expected_sel: &Selection, - ) { - let pairs = AutoPairs::new(pairs.iter()); - let trans = hook(in_doc, in_sel, ch, &pairs).unwrap(); - let mut actual_doc = in_doc.clone(); - assert!(trans.apply(&mut actual_doc)); - assert_eq!(expected_doc, &actual_doc); - assert_eq!(expected_sel, trans.selection().unwrap()); - } - - fn test_hooks_with_pairs( - in_doc: &Rope, - in_sel: &Selection, - test_pairs: I, - pairs: &[(char, char)], - get_expected_doc: F, - actual_sel: &Selection, - ) where - I: IntoIterator, - F: Fn(char, char) -> R, - R: Into, - Rope: From, - { - test_pairs.into_iter().for_each(|(open, close)| { - test_hooks( - in_doc, - in_sel, - *open, - pairs, - &Rope::from(get_expected_doc(*open, *close)), - actual_sel, - ) - }); - } - - // [] indicates range - - /// [] -> insert ( -> ([]) - #[test] - fn test_insert_blank() { - test_hooks_with_pairs( - &Rope::from(LINE_END), - &Selection::single(1, 0), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, close| format!("{}{}{}", open, close, LINE_END), - &Selection::single(2, 1), - ); - - let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)); - - test_hooks_with_pairs( - &empty_doc, - &Selection::single(empty_doc.len_chars(), LINE_END.len()), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, close| { - format!( - "{line_end}{open}{close}{line_end}", - open = open, - close = close, - line_end = LINE_END - ) - }, - &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1), - ); - } - - #[test] - fn test_insert_before_multi_code_point_graphemes() { - for (_, close) in differing_pairs() { - test_hooks( - &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)), - &Selection::single(13, 6), - *close, - DEFAULT_PAIRS, - &Rope::from(format!("hello {}👨‍👩‍👧‍👦 goodbye{}", close, LINE_END)), - &Selection::single(14, 7), - ); - } - } - - #[test] - fn test_insert_at_end_of_document() { - test_hooks_with_pairs( - &Rope::from(LINE_END), - &Selection::single(LINE_END.len(), LINE_END.len()), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, close| format!("{}{}{}", LINE_END, open, close), - &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1), - ); - - test_hooks_with_pairs( - &Rope::from(format!("foo{}", LINE_END)), - &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, close| format!("foo{}{}{}", LINE_END, open, close), - &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4), - ); - } - - /// [] -> append ( -> ([]) - #[test] - fn test_append_blank() { - test_hooks_with_pairs( - // this is what happens when you have a totally blank document and then append - &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)), - // before inserting the pair, the cursor covers all of both empty lines - &Selection::single(0, LINE_END.len() * 2), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, close| { - format!( - "{line_end}{open}{close}{line_end}", - line_end = LINE_END, - open = open, - close = close - ) - }, - // after inserting pair, the cursor covers the first new line and the open char - &Selection::single(0, LINE_END.len() + 2), - ); - } - - /// [] ([]) - /// [] -> insert -> ([]) - /// [] ([]) - #[test] - fn test_insert_blank_multi_cursor() { - test_hooks_with_pairs( - &Rope::from("\n\n\n"), - &Selection::new( - smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),), - 0, - ), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, close| { - format!( - "{open}{close}\n{open}{close}\n{open}{close}\n", - open = open, - close = close - ) - }, - &Selection::new( - smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), - 0, - ), - ); - } - - /// fo[o] -> append ( -> fo[o(]) - #[test] - fn test_append() { - test_hooks_with_pairs( - &Rope::from("foo\n"), - &Selection::single(2, 4), - differing_pairs(), - DEFAULT_PAIRS, - |open, close| format!("foo{}{}\n", open, close), - &Selection::single(2, 5), - ); - } - - /// foo[] -> append to end of line ( -> foo([]) - #[test] - fn test_append_single_cursor() { - test_hooks_with_pairs( - &Rope::from(format!("foo{}", LINE_END)), - &Selection::single(3, 3 + LINE_END.len()), - differing_pairs(), - DEFAULT_PAIRS, - |open, close| format!("foo{}{}{}", open, close, LINE_END), - &Selection::single(4, 5), - ); - } - - /// fo[o] fo[o(]) - /// fo[o] -> append ( -> fo[o(]) - /// fo[o] fo[o(]) - #[test] - fn test_append_multi() { - test_hooks_with_pairs( - &Rope::from("foo\nfoo\nfoo\n"), - &Selection::new( - smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)), - 0, - ), - differing_pairs(), - DEFAULT_PAIRS, - |open, close| { - format!( - "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n", - open = open, - close = close - ) - }, - &Selection::new( - smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)), - 0, - ), - ); - } - - /// ([)] -> insert ) -> ()[] - #[test] - fn test_insert_close_inside_pair() { - for (open, close) in DEFAULT_PAIRS { - let doc = Rope::from(format!("{}{}{}", open, close, LINE_END)); - - test_hooks( - &doc, - &Selection::single(2, 1), - *close, - DEFAULT_PAIRS, - &doc, - &Selection::single(2 + LINE_END.len(), 2), - ); - } - } - - /// [(]) -> append ) -> [()] - #[test] - fn test_append_close_inside_pair() { - for (open, close) in DEFAULT_PAIRS { - let doc = Rope::from(format!("{}{}{}", open, close, LINE_END)); - - test_hooks( - &doc, - &Selection::single(0, 2), - *close, - DEFAULT_PAIRS, - &doc, - &Selection::single(0, 2 + LINE_END.len()), - ); - } - } - - /// ([]) ()[] - /// ([]) -> insert ) -> ()[] - /// ([]) ()[] - #[test] - fn test_insert_close_inside_pair_multi_cursor() { - let sel = Selection::new( - smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), - 0, - ); - - let expected_sel = Selection::new( - smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),), - 0, - ); - - for (open, close) in DEFAULT_PAIRS { - let doc = Rope::from(format!( - "{open}{close}\n{open}{close}\n{open}{close}\n", - open = open, - close = close - )); - - test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel); - } - } - - /// [(]) [()] - /// [(]) -> append ) -> [()] - /// [(]) [()] - #[test] - fn test_append_close_inside_pair_multi_cursor() { - let sel = Selection::new( - smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),), - 0, - ); - - let expected_sel = Selection::new( - smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),), - 0, - ); - - for (open, close) in DEFAULT_PAIRS { - let doc = Rope::from(format!( - "{open}{close}\n{open}{close}\n{open}{close}\n", - open = open, - close = close - )); - - test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel); - } - } - - /// ([]) -> insert ( -> (([])) - #[test] - fn test_insert_open_inside_pair() { - let sel = Selection::single(2, 1); - let expected_sel = Selection::single(3, 2); - - for (open, close) in differing_pairs() { - let doc = Rope::from(format!("{}{}", open, close)); - let expected_doc = Rope::from(format!( - "{open}{open}{close}{close}", - open = open, - close = close - )); - - test_hooks( - &doc, - &sel, - *open, - DEFAULT_PAIRS, - &expected_doc, - &expected_sel, - ); - } - } - - /// [word(]) -> append ( -> [word((])) - #[test] - fn test_append_open_inside_pair() { - let sel = Selection::single(0, 6); - let expected_sel = Selection::single(0, 7); - - for (open, close) in differing_pairs() { - let doc = Rope::from(format!("word{}{}", open, close)); - let expected_doc = Rope::from(format!( - "word{open}{open}{close}{close}", - open = open, - close = close - )); - - test_hooks( - &doc, - &sel, - *open, - DEFAULT_PAIRS, - &expected_doc, - &expected_sel, - ); - } - } - - /// ([]) -> insert " -> ("[]") - #[test] - fn test_insert_nested_open_inside_pair() { - let sel = Selection::single(2, 1); - let expected_sel = Selection::single(3, 2); - - for (outer_open, outer_close) in differing_pairs() { - let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); - - for (inner_open, inner_close) in matching_pairs() { - let expected_doc = Rope::from(format!( - "{}{}{}{}", - outer_open, inner_open, inner_close, outer_close - )); - - test_hooks( - &doc, - &sel, - *inner_open, - DEFAULT_PAIRS, - &expected_doc, - &expected_sel, - ); - } - } - } - - /// [(]) -> append " -> [("]") - #[test] - fn test_append_nested_open_inside_pair() { - let sel = Selection::single(0, 2); - let expected_sel = Selection::single(0, 3); - - for (outer_open, outer_close) in differing_pairs() { - let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); - - for (inner_open, inner_close) in matching_pairs() { - let expected_doc = Rope::from(format!( - "{}{}{}{}", - outer_open, inner_open, inner_close, outer_close - )); - - test_hooks( - &doc, - &sel, - *inner_open, - DEFAULT_PAIRS, - &expected_doc, - &expected_sel, - ); - } - } - } - - /// []word -> insert ( -> ([]word - #[test] - fn test_insert_open_before_non_pair() { - test_hooks_with_pairs( - &Rope::from("word"), - &Selection::single(1, 0), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, _| format!("{}word", open), - &Selection::single(2, 1), - ) - } - - /// [wor]d -> insert ( -> ([wor]d - #[test] - fn test_insert_open_with_selection() { - test_hooks_with_pairs( - &Rope::from("word"), - &Selection::single(3, 0), - DEFAULT_PAIRS, - DEFAULT_PAIRS, - |open, _| format!("{}word", open), - &Selection::single(4, 1), - ) - } - - /// [wor]d -> append ) -> [wor)]d - #[test] - fn test_append_close_inside_non_pair_with_selection() { - let sel = Selection::single(0, 4); - let expected_sel = Selection::single(0, 5); - - for (_, close) in DEFAULT_PAIRS { - let doc = Rope::from("word"); - let expected_doc = Rope::from(format!("wor{}d", close)); - test_hooks( - &doc, - &sel, - *close, - DEFAULT_PAIRS, - &expected_doc, - &expected_sel, - ); - } - } - - /// foo[ wor]d -> insert ( -> foo([) wor]d - #[test] - fn test_insert_open_trailing_word_with_selection() { - test_hooks_with_pairs( - &Rope::from("foo word"), - &Selection::single(7, 3), - differing_pairs(), - DEFAULT_PAIRS, - |open, close| format!("foo{}{} word", open, close), - &Selection::single(9, 4), - ) - } - - /// foo([) wor]d -> insert ) -> foo()[ wor]d - #[test] - fn test_insert_close_inside_pair_trailing_word_with_selection() { - for (open, close) in differing_pairs() { - test_hooks( - &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)), - &Selection::single(9, 4), - *close, - DEFAULT_PAIRS, - &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)), - &Selection::single(9, 5), - ) - } - } - - /// we want pairs that are *not* the same char to be inserted after - /// a non-pair char, for cases like functions, but for pairs that are - /// the same char, we want to *not* insert a pair to handle cases like "I'm" - /// - /// word[] -> insert ( -> word([]) - /// word[] -> insert ' -> word'[] - #[test] - fn test_insert_open_after_non_pair() { - let doc = Rope::from(format!("word{}", LINE_END)); - let sel = Selection::single(5, 4); - let expected_sel = Selection::single(6, 5); - - test_hooks_with_pairs( - &doc, - &sel, - differing_pairs(), - DEFAULT_PAIRS, - |open, close| format!("word{}{}{}", open, close, LINE_END), - &expected_sel, - ); - - test_hooks_with_pairs( - &doc, - &sel, - matching_pairs(), - DEFAULT_PAIRS, - |open, _| format!("word{}{}", open, LINE_END), - &expected_sel, - ); - } - - #[test] - fn test_configured_pairs() { - let test_pairs = &[('`', ':'), ('+', '-')]; - - test_hooks_with_pairs( - &Rope::from(LINE_END), - &Selection::single(1, 0), - test_pairs, - test_pairs, - |open, close| format!("{}{}{}", open, close, LINE_END), - &Selection::single(2, 1), - ); - - let doc = Rope::from(format!("foo`: word{}", LINE_END)); - - test_hooks( - &doc, - &Selection::single(9, 4), - ':', - test_pairs, - &doc, - &Selection::single(9, 5), - ) - } -} diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs index 45503107c721d..3e54d2c245f3a 100644 --- a/helix-core/src/test.rs +++ b/helix-core/src/test.rs @@ -34,7 +34,7 @@ pub fn print(s: &str) -> (String, Selection) { let mut left = String::with_capacity(s.len()); 'outer: while let Some(c) = iter.next() { - let start = left.len(); + let start = left.chars().count(); if c != '#' { left.push(c); @@ -63,6 +63,7 @@ pub fn print(s: &str) -> (String, Selection) { left.push(c); continue; } + if !head_at_beg { let prev = left.pop().unwrap(); if prev != '|' { @@ -71,15 +72,18 @@ pub fn print(s: &str) -> (String, Selection) { continue; } } + iter.next(); // skip "#" if is_primary { primary_idx = Some(ranges.len()); } + let (anchor, head) = match head_at_beg { - true => (left.len(), start), - false => (start, left.len()), + true => (left.chars().count(), start), + false => (start, left.chars().count()), }; + ranges.push(Range::new(anchor, head)); continue 'outer; } @@ -95,6 +99,7 @@ pub fn print(s: &str) -> (String, Selection) { Some(i) => i, None => panic!("missing primary `#[|]#` {:?}", s), }; + let selection = Selection::new(ranges, primary); (left, selection) } @@ -141,3 +146,119 @@ pub fn plain(s: &str, selection: Selection) -> String { } out } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn print_single() { + assert_eq!( + (String::from("hello"), Selection::single(1, 0)), + print("#[|h]#ello") + ); + assert_eq!( + (String::from("hello"), Selection::single(0, 1)), + print("#[h|]#ello") + ); + assert_eq!( + (String::from("hello"), Selection::single(4, 0)), + print("#[|hell]#o") + ); + assert_eq!( + (String::from("hello"), Selection::single(0, 4)), + print("#[hell|]#o") + ); + assert_eq!( + (String::from("hello"), Selection::single(5, 0)), + print("#[|hello]#") + ); + assert_eq!( + (String::from("hello"), Selection::single(0, 5)), + print("#[hello|]#") + ); + } + + #[test] + fn print_multi() { + assert_eq!( + ( + String::from("hello"), + Selection::new( + SmallVec::from_slice(&[Range::new(1, 0), Range::new(5, 4)]), + 0 + ) + ), + print("#[|h]#ell#(|o)#") + ); + assert_eq!( + ( + String::from("hello"), + Selection::new( + SmallVec::from_slice(&[Range::new(0, 1), Range::new(4, 5)]), + 0 + ) + ), + print("#[h|]#ell#(o|)#") + ); + assert_eq!( + ( + String::from("hello"), + Selection::new( + SmallVec::from_slice(&[Range::new(2, 0), Range::new(5, 3)]), + 0 + ) + ), + print("#[|he]#l#(|lo)#") + ); + assert_eq!( + ( + String::from("hello\r\nhello\r\nhello\r\n"), + Selection::new( + SmallVec::from_slice(&[ + Range::new(7, 5), + Range::new(21, 19), + Range::new(14, 12) + ]), + 0 + ) + ), + print("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#") + ); + } + + #[test] + fn print_multi_byte_code_point() { + assert_eq!( + (String::from("„“"), Selection::single(1, 0)), + print("#[|„]#“") + ); + assert_eq!( + (String::from("„“"), Selection::single(2, 1)), + print("„#[|“]#") + ); + assert_eq!( + (String::from("„“"), Selection::single(0, 1)), + print("#[„|]#“") + ); + assert_eq!( + (String::from("„“"), Selection::single(1, 2)), + print("„#[“|]#") + ); + assert_eq!( + (String::from("they said „hello“"), Selection::single(11, 10)), + print("they said #[|„]#hello“") + ); + } + + #[test] + fn print_multi_code_point_grapheme() { + assert_eq!( + ( + String::from("hello 👨‍👩‍👧‍👦 goodbye"), + Selection::single(13, 6) + ), + print("hello #[|👨‍👩‍👧‍👦]# goodbye") + ); + } +} diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs index caf80bd452015..f2ab49c7f1781 100644 --- a/helix-term/tests/test/auto_pairs.rs +++ b/helix-term/tests/test/auto_pairs.rs @@ -1,22 +1,547 @@ +use helix_core::{auto_pairs::DEFAULT_PAIRS, hashmap}; + use super::*; +const LINE_END: &str = helix_core::DEFAULT_LINE_ENDING.as_str(); + +fn differing_pairs() -> impl Iterator { + DEFAULT_PAIRS.iter().filter(|(open, close)| open != close) +} + +fn matching_pairs() -> impl Iterator { + DEFAULT_PAIRS.iter().filter(|(open, close)| open == close) +} + #[tokio::test] -async fn auto_pairs_basic() -> anyhow::Result<()> { - test(("#[\n|]#", "i(", "(#[|)]#\n")).await?; +async fn insert_basic() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("#[{}|]#", LINE_END), + format!("i{}", pair.0), + format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END), + )) + .await?; + } - test_with_config( - Args::default(), - Config { - editor: helix_view::editor::Config { - auto_pairs: AutoPairConfig::Enable(false), - ..Default::default() - }, + Ok(()) +} + +#[tokio::test] +async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> { + // NOTE: these are multi-byte Unicode characters + let pairs = hashmap!('„' => '“', '‚' => '‘', '「' => '」'); + + let config = Config { + editor: helix_view::editor::Config { + auto_pairs: AutoPairConfig::Pairs(pairs.clone()), ..Default::default() }, - helpers::test_syntax_conf(None), - ("#[\n|]#", "i(", "(#[|\n]#"), - ) - .await?; + ..Default::default() + }; + + for (open, close) in pairs.iter() { + test_with_config( + Args::default(), + config.clone(), + helpers::test_syntax_conf(None), + ( + format!("#[{}|]#", LINE_END), + format!("i{}", open), + format!("{}#[|{}]#{}", open, close, LINE_END), + ), + ) + .await?; + + test_with_config( + Args::default(), + config.clone(), + helpers::test_syntax_conf(None), + ( + format!("{}#[{}|]#{}", open, close, LINE_END), + format!("i{}", close), + format!("{}{}#[|{}]#", open, close, LINE_END), + ), + ) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_after_word() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("foo#[{}|]#", LINE_END), + format!("i{}", pair.0), + format!("foo{}#[|{}]#{}", pair.0, pair.1, LINE_END), + )) + .await?; + } + + for pair in matching_pairs() { + test(( + format!("foo#[{}|]#", LINE_END), + format!("i{}", pair.0), + format!("foo{}#[|{}]#", pair.0, LINE_END), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_before_word() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("#[f|]#oo{}", LINE_END), + format!("i{}", pair.0), + format!("{}#[|f]#oo{}", pair.0, LINE_END), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_before_word_selection() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("#[foo|]#{}", LINE_END), + format!("i{}", pair.0), + format!("{}#[|foo]#{}", pair.0, LINE_END), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_before_word_selection_trailing_word() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("foo#[ wor|]#{}", LINE_END), + format!("i{}", pair.0), + format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_closer_selection_trailing_word() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END), + format!("i{}", pair.1), + format!("foo{}{}#[| wor]#{}", pair.0, pair.1, LINE_END), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_before_eol() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("{0}#[{0}|]#", LINE_END), + format!("i{}", pair.0), + format!( + "{eol}{open}#[|{close}]#{eol}", + eol = LINE_END, + open = pair.0, + close = pair.1 + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_auto_pairs_disabled() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test_with_config( + Args::default(), + Config { + editor: helix_view::editor::Config { + auto_pairs: AutoPairConfig::Enable(false), + ..Default::default() + }, + ..Default::default() + }, + helpers::test_syntax_conf(None), + ( + format!("#[{}|]#", LINE_END), + format!("i{}", pair.0), + format!("{}#[|{}]#", pair.0, LINE_END), + ), + ) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_multi_range() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("#[{eol}|]##({eol}|)##({eol}|)#", eol = LINE_END), + format!("i{}", pair.0), + format!( + "{open}#[|{close}]#{eol}{open}#(|{close})#{eol}{open}#(|{close})#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_before_multi_code_point_graphemes() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("hello #[👨‍👩‍👧‍👦|]# goodbye{}", LINE_END), + format!("i{}", pair.1), + format!("hello {}#[|👨‍👩‍👧‍👦]# goodbye{}", pair.1, LINE_END), + )) + .await?; + } + Ok(()) +} + +#[tokio::test] +async fn insert_at_end_of_document() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(TestCase { + in_text: String::from(LINE_END), + in_selection: Selection::single(LINE_END.len(), LINE_END.len()), + in_keys: format!("i{}", pair.0), + out_text: format!("{}{}{}", LINE_END, pair.0, pair.1), + out_selection: Selection::single(LINE_END.len() + 1, LINE_END.len() + 2), + }) + .await?; + + test(TestCase { + in_text: format!("foo{}", LINE_END), + in_selection: Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()), + in_keys: format!("i{}", pair.0), + out_text: format!("foo{}{}{}", LINE_END, pair.0, pair.1), + out_selection: Selection::single(LINE_END.len() + 4, LINE_END.len() + 5), + }) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_close_inside_pair() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!( + "{open}#[{close}|]#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + format!("i{}", pair.1), + format!( + "{open}{close}#[|{eol}]#", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_close_inside_pair_multi() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!( + "{open}#[{close}|]#{eol}{open}#({close}|)#{eol}{open}#({close}|)#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + format!("i{}", pair.1), + format!( + "{open}{close}#[|{eol}]#{open}{close}#(|{eol})#{open}{close}#(|{eol})#", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_nested_open_inside_pair() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!( + "{open}#[{close}|]#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + format!("i{}", pair.0), + format!( + "{open}{open}#[|{close}]#{close}{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn insert_nested_open_inside_pair_multi() -> anyhow::Result<()> { + for outer_pair in DEFAULT_PAIRS { + for inner_pair in DEFAULT_PAIRS { + if inner_pair.0 == outer_pair.0 { + continue; + } + + test(( + format!( + "{outer_open}#[{outer_close}|]#{eol}{outer_open}#({outer_close}|)#{eol}{outer_open}#({outer_close}|)#{eol}", + outer_open = outer_pair.0, + outer_close = outer_pair.1, + eol = LINE_END + ), + format!("i{}", inner_pair.0), + format!( + "{outer_open}{inner_open}#[|{inner_close}]#{outer_close}{eol}{outer_open}{inner_open}#(|{inner_close})#{outer_close}{eol}{outer_open}{inner_open}#(|{inner_close})#{outer_close}{eol}", + outer_open = outer_pair.0, + outer_close = outer_pair.1, + inner_open = inner_pair.0, + inner_close = inner_pair.1, + eol = LINE_END + ), + )) + .await?; + } + } + + Ok(()) +} + +#[tokio::test] +async fn append_basic() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("#[{}|]#", LINE_END), + format!("a{}", pair.0), + format!( + "#[{eol}{open}{close}|]#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_multi_range() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("#[ |]#{eol}#( |)#{eol}#( |)#{eol}", eol = LINE_END), + format!("a{}", pair.0), + format!( + "#[ {open}{close}|]#{eol}#( {open}{close}|)#{eol}#( {open}{close}|)#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_close_inside_pair() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!( + "#[{open}|]#{close}{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + format!("a{}", pair.1), + format!( + "#[{open}{close}{eol}|]#", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_close_inside_pair_multi() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!( + "#[{open}|]#{close}{eol}#({open}|)#{close}{eol}#({open}|)#{close}{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + format!("a{}", pair.1), + format!( + "#[{open}{close}{eol}|]##({open}{close}{eol}|)##({open}{close}{eol}|)#", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_end_of_word() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("fo#[o|]#{}", LINE_END), + format!("a{}", pair.0), + format!( + "fo#[o{open}{close}|]#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_middle_of_word() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("#[wo|]#rd{}", LINE_END), + format!("a{}", pair.1), + format!("#[wo{}r|]#d{}", pair.1, LINE_END), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_end_of_word_multi() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("fo#[o|]#{eol}fo#(o|)#{eol}fo#(o|)#{eol}", eol = LINE_END), + format!("a{}", pair.0), + format!( + "fo#[o{open}{close}|]#{eol}fo#(o{open}{close}|)#{eol}fo#(o{open}{close}|)#{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_inside_nested_pair() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!( + "f#[oo{open}|]#{close}{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + format!("a{}", pair.0), + format!( + "f#[oo{open}{open}{close}|]#{close}{eol}", + open = pair.0, + close = pair.1, + eol = LINE_END + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn append_inside_nested_pair_multi() -> anyhow::Result<()> { + for outer_pair in DEFAULT_PAIRS { + for inner_pair in DEFAULT_PAIRS { + if inner_pair.0 == outer_pair.0 { + continue; + } + + test(( + format!( + "f#[oo{outer_open}|]#{outer_close}{eol}f#(oo{outer_open}|)#{outer_close}{eol}f#(oo{outer_open}|)#{outer_close}{eol}", + outer_open = outer_pair.0, + outer_close = outer_pair.1, + eol = LINE_END + ), + format!("a{}", inner_pair.0), + format!( + "f#[oo{outer_open}{inner_open}{inner_close}|]#{outer_close}{eol}f#(oo{outer_open}{inner_open}{inner_close}|)#{outer_close}{eol}f#(oo{outer_open}{inner_open}{inner_close}|)#{outer_close}{eol}", + outer_open = outer_pair.0, + outer_close = outer_pair.1, + inner_open = inner_pair.0, + inner_close = inner_pair.1, + eol = LINE_END + ), + )) + .await?; + } + } Ok(()) } diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index f3d9473469794..ad6f621a5ae73 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -17,7 +17,7 @@ pub trait ClipboardProvider: std::fmt::Debug { #[cfg(not(windows))] macro_rules! command_provider { (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{ - log::info!( + log::debug!( "Using {} to interact with the system clipboard", if $set_prg != $get_prg { format!("{}+{}", $set_prg, $get_prg)} else { $set_prg.to_string() } ); From fbf8078611abc2fa62856c84b758f2943c00cdec Mon Sep 17 00:00:00 2001 From: Greg Troszak Date: Thu, 20 Oct 2022 20:39:30 -0400 Subject: [PATCH 087/151] Clarify use of `HELIX_RUNTIME` (#4382) --- README.md | 11 +++++++++-- book/src/install.md | 11 ++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5847a612e5050..d09bbad573378 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ cd helix cargo install --path helix-term ``` -This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars. +This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`. Helix needs its runtime files so make sure to copy/symlink the `runtime/` directory into the config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows). @@ -68,7 +68,14 @@ cd %appdata%\helix mklink /D runtime "\runtime" ``` -This location can be overridden via the `HELIX_RUNTIME` environment variable. +The runtime location can be overridden via the `HELIX_RUNTIME` environment variable. + +> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term`, +> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`. + +If you plan on keeping the repo locally, an alternative to copying/symlinking +runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime` +(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory). Packages already solve this for you by wrapping the `hx` binary with a wrapper that sets the variable to the install dir. diff --git a/book/src/install.md b/book/src/install.md index 4e7ea8dcc3ade..6e2a1f3dfafc5 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -75,7 +75,7 @@ cd helix cargo install --path helix-term ``` -This will install the `hx` binary to `$HOME/.cargo/bin`. +This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`. Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden @@ -103,6 +103,15 @@ cd %appdata%\helix mklink /D runtime "\runtime" ``` +The runtime location can be overridden via the `HELIX_RUNTIME` environment variable. + +> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term`, +> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`. + +If you plan on keeping the repo locally, an alternative to copying/symlinking +runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime` +(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory). + To use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder: ```bash From ce399471f047c241c63caba11ce154776df5024c Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:58:13 -0400 Subject: [PATCH 088/151] simplify encoding test macro (#4385) --- helix-view/src/document.rs | 108 ++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 63 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index a723b89b59a3f..ca07431b20e50 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1354,84 +1354,66 @@ mod test { ); } - macro_rules! test_decode { - ($label:expr, $label_override:expr) => { - let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap(); - let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding"); - let path = base_path.join(format!("{}_in.txt", $label)); - let ref_path = base_path.join(format!("{}_in_ref.txt", $label)); - assert!(path.exists()); - assert!(ref_path.exists()); - - let mut file = std::fs::File::open(path).unwrap(); - let text = from_reader(&mut file, Some(encoding)) - .unwrap() - .0 - .to_string(); - let expectation = std::fs::read_to_string(ref_path).unwrap(); - assert_eq!(text[..], expectation[..]); - }; - } - - macro_rules! test_encode { - ($label:expr, $label_override:expr) => { - let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap(); - let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding"); - let path = base_path.join(format!("{}_out.txt", $label)); - let ref_path = base_path.join(format!("{}_out_ref.txt", $label)); - assert!(path.exists()); - assert!(ref_path.exists()); - - let text = Rope::from_str(&std::fs::read_to_string(path).unwrap()); - let mut buf: Vec = Vec::new(); - helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap(); - - let expectation = std::fs::read(ref_path).unwrap(); - assert_eq!(buf, expectation); - }; - } - - macro_rules! test_decode_fn { + macro_rules! decode { ($name:ident, $label:expr, $label_override:expr) => { #[test] fn $name() { - test_decode!($label, $label_override); - } + let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap(); + let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding"); + let path = base_path.join(format!("{}_in.txt", $label)); + let ref_path = base_path.join(format!("{}_in_ref.txt", $label)); + assert!(path.exists()); + assert!(ref_path.exists()); + + let mut file = std::fs::File::open(path).unwrap(); + let text = from_reader(&mut file, Some(encoding)) + .unwrap() + .0 + .to_string(); + let expectation = std::fs::read_to_string(ref_path).unwrap(); + assert_eq!(text[..], expectation[..]); + } }; ($name:ident, $label:expr) => { - #[test] - fn $name() { - test_decode!($label, $label); - } + decode!($name, $label, $label); }; } - macro_rules! test_encode_fn { + macro_rules! encode { ($name:ident, $label:expr, $label_override:expr) => { #[test] fn $name() { - test_encode!($label, $label_override); + let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap(); + let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding"); + let path = base_path.join(format!("{}_out.txt", $label)); + let ref_path = base_path.join(format!("{}_out_ref.txt", $label)); + assert!(path.exists()); + assert!(ref_path.exists()); + + let text = Rope::from_str(&std::fs::read_to_string(path).unwrap()); + let mut buf: Vec = Vec::new(); + helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap(); + + let expectation = std::fs::read(ref_path).unwrap(); + assert_eq!(buf, expectation); } }; ($name:ident, $label:expr) => { - #[test] - fn $name() { - test_encode!($label, $label); - } + encode!($name, $label, $label); }; } - test_decode_fn!(test_big5_decode, "big5"); - test_encode_fn!(test_big5_encode, "big5"); - test_decode_fn!(test_euc_kr_decode, "euc_kr", "EUC-KR"); - test_encode_fn!(test_euc_kr_encode, "euc_kr", "EUC-KR"); - test_decode_fn!(test_gb18030_decode, "gb18030"); - test_encode_fn!(test_gb18030_encode, "gb18030"); - test_decode_fn!(test_iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP"); - test_encode_fn!(test_iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP"); - test_decode_fn!(test_jis0208_decode, "jis0208", "EUC-JP"); - test_encode_fn!(test_jis0208_encode, "jis0208", "EUC-JP"); - test_decode_fn!(test_jis0212_decode, "jis0212", "EUC-JP"); - test_decode_fn!(test_shift_jis_decode, "shift_jis"); - test_encode_fn!(test_shift_jis_encode, "shift_jis"); + decode!(big5_decode, "big5"); + encode!(big5_encode, "big5"); + decode!(euc_kr_decode, "euc_kr", "EUC-KR"); + encode!(euc_kr_encode, "euc_kr", "EUC-KR"); + decode!(gb18030_decode, "gb18030"); + encode!(gb18030_encode, "gb18030"); + decode!(iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP"); + encode!(iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP"); + decode!(jis0208_decode, "jis0208", "EUC-JP"); + encode!(jis0208_encode, "jis0208", "EUC-JP"); + decode!(jis0212_decode, "jis0212", "EUC-JP"); + decode!(shift_jis_decode, "shift_jis"); + encode!(shift_jis_encode, "shift_jis"); } From 8673c1ec0cb4074a1082d4681b655110d5b7facb Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 7 Oct 2022 01:44:53 +0200 Subject: [PATCH 089/151] sort codeaction by their kind instead of alphabetically --- helix-term/src/commands/lsp.rs | 84 ++++++++++++++++++++++++--------- helix-term/src/ui/completion.rs | 2 +- helix-term/src/ui/menu.rs | 9 +++- 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 3fa5c96fff83b..5fec38d4b06d6 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -452,43 +452,83 @@ pub fn code_action(cx: &mut Context) { cx.callback( future, move |editor, compositor, response: Option| { - let actions = match response { + let mut actions = match response { Some(a) => a, None => return, }; + if actions.is_empty() { editor.set_status("No code actions available"); return; } - let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { - if event != PromptEvent::Validate { - return; - } + // sort by CodeActionKind + // this ensures that the most relevant codeactions (quickfix) show up first + // while more situational commands (like refactors) show up later + // this behaviour is modeled after the behaviour of vscode (editor/contrib/codeAction/browser/codeActionWidget.ts) + + let mut categories = vec![Vec::new(); 8]; + for action in actions.drain(..) { + let category = match &action { + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + kind: Some(kind), + .. + }) => { + let mut components = kind.as_str().split('.'); + + match components.next() { + Some("quickfix") => 0, + Some("refactor") => match components.next() { + Some("extract") => 1, + Some("inline") => 2, + Some("rewrite") => 3, + Some("move") => 4, + Some("surround") => 5, + _ => 7, + }, + Some("source") => 6, + _ => 7, + } + } + _ => 7, + }; + + categories[category].push(action); + } - // always present here - let code_action = code_action.unwrap(); + for category in categories { + actions.extend(category.into_iter()) + } - match code_action { - lsp::CodeActionOrCommand::Command(command) => { - log::debug!("code action command: {:?}", command); - execute_lsp_command(editor, command.clone()); + let mut picker = + ui::Menu::new(actions, false, (), move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; } - lsp::CodeActionOrCommand::CodeAction(code_action) => { - log::debug!("code action: {:?}", code_action); - if let Some(ref workspace_edit) = code_action.edit { - log::debug!("edit: {:?}", workspace_edit); - apply_workspace_edit(editor, offset_encoding, workspace_edit); - } - // if code action provides both edit and command first the edit - // should be applied and then the command - if let Some(command) = &code_action.command { + // always present here + let code_action = code_action.unwrap(); + + match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); execute_lsp_command(editor, command.clone()); } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(editor, offset_encoding, workspace_edit); + } + + // if code action provides both edit and command first the edit + // should be applied and then the command + if let Some(command) = &code_action.command { + execute_lsp_command(editor, command.clone()); + } + } } - } - }); + }); picker.move_down(); // pre-select the first item let popup = Popup::new("code-action", picker); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 7348dcf444316..2e555e4b5ed3d 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -97,7 +97,7 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Self { - let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, item: &CompletionItem, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index f77f5e8054a42..b60e64546e251 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -74,6 +74,7 @@ impl Menu { // rendering) pub fn new( options: Vec, + sort: bool, editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { @@ -91,8 +92,12 @@ impl Menu { recalculate: true, }; - // TODO: scoring on empty input should just use a fastpath - menu.score(""); + if sort { + // TODO: scoring on empty input should just use a fastpath + menu.score(""); + } else { + menu.matches = (0..menu.options.len()).map(|i| (i, 0)).collect(); + } menu } From c70d762a7b6285684dd7c5f39a53f05394369981 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 7 Oct 2022 12:15:52 +0200 Subject: [PATCH 090/151] sort autocompletins by fuzzy match --- helix-term/src/ui/menu.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index b60e64546e251..f7712e4dc5dad 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -117,10 +117,7 @@ impl Menu { .map(|score| (index, score)) }), ); - // matches.sort_unstable_by_key(|(_, score)| -score); - self.matches.sort_unstable_by_key(|(index, _score)| { - self.options[*index].sort_text(&self.editor_data) - }); + self.matches.sort_unstable_by_key(|(_, score)| -score); // reset cursor position self.cursor = None; From 8d8b5d6624901f34e99407e18fea0b8abbb058aa Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sat, 8 Oct 2022 14:18:53 +0200 Subject: [PATCH 091/151] use stable sort instead of allocating new vectors --- helix-term/src/commands/lsp.rs | 50 ++++++++++++++-------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 5fec38d4b06d6..add6183f51f48 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -467,38 +467,28 @@ pub fn code_action(cx: &mut Context) { // while more situational commands (like refactors) show up later // this behaviour is modeled after the behaviour of vscode (editor/contrib/codeAction/browser/codeActionWidget.ts) - let mut categories = vec![Vec::new(); 8]; - for action in actions.drain(..) { - let category = match &action { - lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { - kind: Some(kind), - .. - }) => { - let mut components = kind.as_str().split('.'); - - match components.next() { - Some("quickfix") => 0, - Some("refactor") => match components.next() { - Some("extract") => 1, - Some("inline") => 2, - Some("rewrite") => 3, - Some("move") => 4, - Some("surround") => 5, - _ => 7, - }, - Some("source") => 6, + actions.sort_by_key(|action| match &action { + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + kind: Some(kind), .. + }) => { + let mut components = kind.as_str().split('.'); + + match components.next() { + Some("quickfix") => 0, + Some("refactor") => match components.next() { + Some("extract") => 1, + Some("inline") => 2, + Some("rewrite") => 3, + Some("move") => 4, + Some("surround") => 5, _ => 7, - } + }, + Some("source") => 6, + _ => 7, } - _ => 7, - }; - - categories[category].push(action); - } - - for category in categories { - actions.extend(category.into_iter()) - } + } + _ => 7, + }); let mut picker = ui::Menu::new(actions, false, (), move |editor, code_action, event| { From dc3527f52df27ae20294ca14bac60994725519aa Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 11 Oct 2022 19:37:22 +0200 Subject: [PATCH 092/151] use permalink to vscode repo --- helix-term/src/commands/lsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index add6183f51f48..ecaab169cff53 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -465,7 +465,7 @@ pub fn code_action(cx: &mut Context) { // sort by CodeActionKind // this ensures that the most relevant codeactions (quickfix) show up first // while more situational commands (like refactors) show up later - // this behaviour is modeled after the behaviour of vscode (editor/contrib/codeAction/browser/codeActionWidget.ts) + // this behaviour is modeled after the behaviour of vscode (https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts) actions.sort_by_key(|action| match &action { lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { From 189aa0bfcfb6841ced946289e112cc9cdd4919a5 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 13 Oct 2022 20:10:10 +0200 Subject: [PATCH 093/151] never sort menu items when no fuzzy matching is possible --- helix-term/src/commands/lsp.rs | 45 ++++++++++++++++----------------- helix-term/src/ui/completion.rs | 2 +- helix-term/src/ui/menu.rs | 15 +++-------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index ecaab169cff53..100fa1962acb6 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -490,35 +490,34 @@ pub fn code_action(cx: &mut Context) { _ => 7, }); - let mut picker = - ui::Menu::new(actions, false, (), move |editor, code_action, event| { - if event != PromptEvent::Validate { - return; - } + let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; + } - // always present here - let code_action = code_action.unwrap(); + // always present here + let code_action = code_action.unwrap(); - match code_action { - lsp::CodeActionOrCommand::Command(command) => { - log::debug!("code action command: {:?}", command); - execute_lsp_command(editor, command.clone()); + match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); + execute_lsp_command(editor, command.clone()); + } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(editor, offset_encoding, workspace_edit); } - lsp::CodeActionOrCommand::CodeAction(code_action) => { - log::debug!("code action: {:?}", code_action); - if let Some(ref workspace_edit) = code_action.edit { - log::debug!("edit: {:?}", workspace_edit); - apply_workspace_edit(editor, offset_encoding, workspace_edit); - } - // if code action provides both edit and command first the edit - // should be applied and then the command - if let Some(command) = &code_action.command { - execute_lsp_command(editor, command.clone()); - } + // if code action provides both edit and command first the edit + // should be applied and then the command + if let Some(command) = &code_action.command { + execute_lsp_command(editor, command.clone()); } } - }); + } + }); picker.move_down(); // pre-select the first item let popup = Popup::new("code-action", picker); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2e555e4b5ed3d..7348dcf444316 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -97,7 +97,7 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Self { - let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, item: &CompletionItem, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index f7712e4dc5dad..4b1155e3e75c9 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -74,15 +74,15 @@ impl Menu { // rendering) pub fn new( options: Vec, - sort: bool, editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { - let mut menu = Self { + let matches = (0..options.len()).map(|i| (i, 0)).collect(); + Self { options, editor_data, matcher: Box::new(Matcher::default()), - matches: Vec::new(), + matches, cursor: None, widths: Vec::new(), callback_fn: Box::new(callback_fn), @@ -90,16 +90,7 @@ impl Menu { size: (0, 0), viewport: (0, 0), recalculate: true, - }; - - if sort { - // TODO: scoring on empty input should just use a fastpath - menu.score(""); - } else { - menu.matches = (0..menu.options.len()).map(|i| (i, 0)).collect(); } - - menu } pub fn score(&mut self, pattern: &str) { From 9af7c1c9f3aa498719bd0fa39d4d35746f23adaa Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 14 Oct 2022 11:50:09 +0200 Subject: [PATCH 094/151] Sort by fixed diagnostics/is_preffered within codeaction categories --- helix-term/src/commands/lsp.rs | 128 +++++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 28 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 100fa1962acb6..987fc4ce170ef 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,6 +1,6 @@ use helix_lsp::{ block_on, - lsp::{self, DiagnosticSeverity, NumberOrString}, + lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString}, util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; @@ -18,7 +18,7 @@ use crate::{ }, }; -use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc}; +use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, path::PathBuf, sync::Arc}; /// Gets the language server that is attached to a document, and /// if it's not active displays a status message. Using this macro @@ -211,7 +211,6 @@ fn sym_picker( Ok(path) => path, Err(_) => { let err = format!("unable to convert URI to filepath: {}", uri); - log::error!("{}", err); cx.editor.set_error(err); return; } @@ -421,6 +420,63 @@ impl ui::menu::Item for lsp::CodeActionOrCommand { } } +/// Determines the category of the `CodeAction` using the `CodeAction::kind` field. +/// Returns a number that represent these categories. +/// Categories with a lower number should be displayed first. +/// +/// +/// While the `kind` field is defined as open ended in the LSP spec (any value may be used) +/// in practice a closed set of common values (mostly suggested in the LSP spec) are used. +/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker) +/// to make them easier to navigate. Helix does not display these headings to the user. +/// However it does sort code actions by their categories to achieve the same order as the VScode picker, +/// just without the headings. +/// +/// The order used here is modeled after the [vscode sourcecode](https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts>) +fn action_category(action: &CodeActionOrCommand) -> u32 { + if let CodeActionOrCommand::CodeAction(CodeAction { + kind: Some(kind), .. + }) = action + { + let mut components = kind.as_str().split('.'); + match components.next() { + Some("quickfix") => 0, + Some("refactor") => match components.next() { + Some("extract") => 1, + Some("inline") => 2, + Some("rewrite") => 3, + Some("move") => 4, + Some("surround") => 5, + _ => 7, + }, + Some("source") => 6, + _ => 7, + } + } else { + 7 + } +} + +fn action_prefered(action: &CodeActionOrCommand) -> bool { + matches!( + action, + CodeActionOrCommand::CodeAction(CodeAction { + is_preferred: Some(true), + .. + }) + ) +} + +fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool { + matches!( + action, + CodeActionOrCommand::CodeAction(CodeAction { + diagnostics: Some(diagnostics), + .. + }) if !diagnostics.is_empty() + ) +} + pub fn code_action(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -457,37 +513,53 @@ pub fn code_action(cx: &mut Context) { None => return, }; + // remove disabled code actions + actions.retain(|action| { + matches!( + action, + CodeActionOrCommand::Command(_) + | CodeActionOrCommand::CodeAction(CodeAction { disabled: None, .. }) + ) + }); + if actions.is_empty() { editor.set_status("No code actions available"); return; } - // sort by CodeActionKind - // this ensures that the most relevant codeactions (quickfix) show up first - // while more situational commands (like refactors) show up later - // this behaviour is modeled after the behaviour of vscode (https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts) - - actions.sort_by_key(|action| match &action { - lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { - kind: Some(kind), .. - }) => { - let mut components = kind.as_str().split('.'); - - match components.next() { - Some("quickfix") => 0, - Some("refactor") => match components.next() { - Some("extract") => 1, - Some("inline") => 2, - Some("rewrite") => 3, - Some("move") => 4, - Some("surround") => 5, - _ => 7, - }, - Some("source") => 6, - _ => 7, - } + // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec. + // Many details are modeled after vscode because langauge servers are usually tested against it. + // VScode sorts the codeaction two times: + // + // First the codeactions that fix some diagnostics are moved to the front. + // If both codeactions fix some diagnostics (or both fix none) the codeaction + // that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate + // submenus that only contain a certain category (see `action_category`) of actions. + // + // Below this done in in a single sorting step + actions.sort_by(|action1, action2| { + // sort actions by category + let order = action_category(action1).cmp(&action_category(action2)); + if order != Ordering::Equal { + return order; } - _ => 7, + // within the categories sort by relevancy. + // Modeled after the `codeActionsComparator` function in vscode: + // https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeAction.ts + + // if one code action fixes a diagnostic but the other one doesn't show it first + let order = action_fixes_diagnostics(action1) + .cmp(&action_fixes_diagnostics(action2)) + .reverse(); + if order != Ordering::Equal { + return order; + } + + // if one of the codeactions is marked as prefered show it first + // otherwise keep the original LSP sorting + action_prefered(action1) + .cmp(&action_prefered(action2)) + .reverse() }); let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { From 4ff5feeb0c6c1ce012aa1179a44f35b6f0df4da1 Mon Sep 17 00:00:00 2001 From: A-Walrus <58790821+A-Walrus@users.noreply.github.com> Date: Fri, 21 Oct 2022 04:06:57 +0300 Subject: [PATCH 095/151] Fix shellwords delimiter handling (#4098) * Fix shellwords delimiter handling This allows commands such as `:set statusline.center ["file-type"]` to work. Before the quotes within the list would mess it up. Also added a test to ensure correct behavior * Rename Delimiter -> OnWhitespace --- helix-core/src/shellwords.rs | 63 +++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 4323039ab5d1e..afc83496f2637 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -3,8 +3,9 @@ use std::borrow::Cow; /// Get the vec of escaped / quoted / doublequoted filenames from the input str pub fn shellwords(input: &str) -> Vec> { enum State { - Normal, - NormalEscaped, + OnWhitespace, + Unquoted, + UnquotedEscaped, Quoted, QuoteEscaped, Dquoted, @@ -13,7 +14,7 @@ pub fn shellwords(input: &str) -> Vec> { use State::*; - let mut state = Normal; + let mut state = Unquoted; let mut args: Vec> = Vec::new(); let mut escaped = String::with_capacity(input.len()); @@ -22,31 +23,47 @@ pub fn shellwords(input: &str) -> Vec> { for (i, c) in input.char_indices() { state = match state { - Normal => match c { + OnWhitespace => match c { + '"' => { + end = i; + Dquoted + } + '\'' => { + end = i; + Quoted + } '\\' => { if cfg!(unix) { escaped.push_str(&input[start..i]); start = i + 1; - NormalEscaped + UnquotedEscaped } else { - Normal + OnWhitespace } } - '"' => { + c if c.is_ascii_whitespace() => { end = i; - Dquoted + OnWhitespace } - '\'' => { - end = i; - Quoted + _ => Unquoted, + }, + Unquoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[start..i]); + start = i + 1; + UnquotedEscaped + } else { + Unquoted + } } c if c.is_ascii_whitespace() => { end = i; - Normal + OnWhitespace } - _ => Normal, + _ => Unquoted, }, - NormalEscaped => Normal, + UnquotedEscaped => Unquoted, Quoted => match c { '\\' => { if cfg!(unix) { @@ -59,7 +76,7 @@ pub fn shellwords(input: &str) -> Vec> { } '\'' => { end = i; - Normal + OnWhitespace } _ => Quoted, }, @@ -76,7 +93,7 @@ pub fn shellwords(input: &str) -> Vec> { } '"' => { end = i; - Normal + OnWhitespace } _ => Dquoted, }, @@ -195,4 +212,18 @@ mod test { ]; assert_eq!(expected, result); } + + #[test] + fn test_lists() { + let input = + r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#; + let result = shellwords(input); + let expected = vec![ + Cow::from(":set"), + Cow::from("statusline.center"), + Cow::from(r#"["file-type","file-encoding"]"#), + Cow::from(r#"["list", "in", "qoutes"]"#), + ]; + assert_eq!(expected, result); + } } From 7e29ee6daeb86fbd20a8b5eaae30b2e0f868147d Mon Sep 17 00:00:00 2001 From: Charlie Groves Date: Thu, 20 Oct 2022 21:35:02 -0400 Subject: [PATCH 096/151] Autosave all when the terminal loses focus (#3178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Autosave all when the terminal loses focus * Correct comment on focus config Co-authored-by: Blaž Hrastnik * Need a block_try_flush_writes in all quit_all paths Co-authored-by: Blaž Hrastnik --- book/src/configuration.md | 1 + helix-term/src/application.rs | 12 ++++-- helix-term/src/commands/typed.rs | 70 ++++++++++++-------------------- helix-term/src/ui/editor.rs | 10 ++++- helix-view/src/editor.rs | 3 ++ 5 files changed, 47 insertions(+), 49 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 996c5fb6ada36..9f3fe2fa95bd2 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -49,6 +49,7 @@ on unix operating systems. | `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "line-numbers"]` | | `auto-completion` | Enable automatic pop up of auto-completion. | `true` | | `auto-format` | Enable automatic formatting on save. | `true` | +| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `auto-info` | Whether to display infoboxes | `true` | diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b4b4a6751bd02..8ee0802f20184 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -37,8 +37,8 @@ use anyhow::{Context, Error}; use crossterm::{ event::{ - DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, - Event as CrosstermEvent, + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, }, execute, terminal, tty::IsTty, @@ -102,6 +102,7 @@ fn restore_term() -> Result<(), Error> { execute!( stdout, DisableBracketedPaste, + DisableFocusChange, terminal::LeaveAlternateScreen )?; terminal::disable_raw_mode()?; @@ -925,7 +926,12 @@ impl Application { async fn claim_term(&mut self) -> Result<(), Error> { terminal::enable_raw_mode()?; let mut stdout = stdout(); - execute!(stdout, terminal::EnterAlternateScreen, EnableBracketedPaste)?; + execute!( + stdout, + terminal::EnterAlternateScreen, + EnableBracketedPaste, + EnableFocusChange + )?; execute!(stdout, terminal::Clear(terminal::ClearType::All))?; if self.config.load().editor.mouse { execute!(stdout, EnableMouseCapture)?; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f20e71c25f232..f154fd3897f61 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -559,17 +559,11 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> Ok(()) } -fn write_all_impl( +pub fn write_all_impl( cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, - quit: bool, force: bool, + write_scratch: bool, ) -> anyhow::Result<()> { - if event != PromptEvent::Validate { - return Ok(()); - } - let mut errors: Vec<&'static str> = Vec::new(); let auto_format = cx.editor.config().auto_format; let jobs = &mut cx.jobs; @@ -580,12 +574,13 @@ fn write_all_impl( .documents .values() .filter_map(|doc| { - if doc.path().is_none() { - errors.push("cannot write a buffer without a filename\n"); + if !doc.is_modified() { return None; } - - if !doc.is_modified() { + if doc.path().is_none() { + if write_scratch { + errors.push("cannot write a buffer without a filename\n"); + } return None; } @@ -611,20 +606,6 @@ fn write_all_impl( cx.editor.save::(id, None, force)?; } - if quit { - cx.block_try_flush_writes()?; - - if !force { - buffers_remaining_impl(cx.editor)?; - } - - // close all views - let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); - for view_id in views { - cx.editor.close(view_id); - } - } - if !errors.is_empty() && !force { bail!("{:?}", errors); } @@ -634,49 +615,50 @@ fn write_all_impl( fn write_all( cx: &mut compositor::Context, - args: &[Cow], + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_all_impl(cx, args, event, false, false) + write_all_impl(cx, false, true) } fn write_all_quit( cx: &mut compositor::Context, - args: &[Cow], + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - - write_all_impl(cx, args, event, true, false) + write_all_impl(cx, false, true)?; + quit_all_impl(cx, false) } fn force_write_all_quit( cx: &mut compositor::Context, - args: &[Cow], + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - - write_all_impl(cx, args, event, true, true) + let _ = write_all_impl(cx, true, true); + quit_all_impl(cx, true) } -fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> { +fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<()> { + cx.block_try_flush_writes()?; if !force { - buffers_remaining_impl(editor)?; + buffers_remaining_impl(cx.editor)?; } // close all views - let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect(); + let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - editor.close(view_id); + cx.editor.close(view_id); } Ok(()) @@ -691,8 +673,7 @@ fn quit_all( return Ok(()); } - cx.block_try_flush_writes()?; - quit_all_impl(cx.editor, false) + quit_all_impl(cx, false) } fn force_quit_all( @@ -704,7 +685,7 @@ fn force_quit_all( return Ok(()); } - quit_all_impl(cx.editor, true) + quit_all_impl(cx, true) } fn cquit( @@ -722,8 +703,7 @@ fn cquit( .unwrap_or(1); cx.editor.exit_code = exit_code; - cx.block_try_flush_writes()?; - quit_all_impl(cx.editor, false) + quit_all_impl(cx, false) } fn force_cquit( @@ -741,7 +721,7 @@ fn force_cquit( .unwrap_or(1); cx.editor.exit_code = exit_code; - quit_all_impl(cx.editor, true) + quit_all_impl(cx, true) } fn theme( @@ -2141,7 +2121,7 @@ pub static TYPABLE_COMMAND_MAP: Lazy self.handle_mouse_event(event, &mut cx), Event::IdleTimeout => self.handle_idle_timeout(&mut cx), - Event::FocusGained | Event::FocusLost => EventResult::Ignored(None), + Event::FocusGained => EventResult::Ignored(None), + Event::FocusLost => { + if context.editor.config().auto_save { + if let Err(e) = commands::typed::write_all_impl(context, false, false) { + context.editor.set_error(format!("{}", e)); + } + } + EventResult::Consumed(None) + } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index fe243b92d181f..af69ceeae19a6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -140,6 +140,8 @@ pub struct Config { pub auto_completion: bool, /// Automatic formatting on save. Defaults to true. pub auto_format: bool, + /// Automatic save on focus lost. Defaults to false. + pub auto_save: bool, /// Time in milliseconds since last keypress before idle timers trigger. /// Used for autocompletion, set to 0 for instant. Defaults to 400ms. #[serde( @@ -592,6 +594,7 @@ impl Default for Config { auto_pairs: AutoPairConfig::default(), auto_completion: true, auto_format: true, + auto_save: false, idle_timeout: Duration::from_millis(400), completion_trigger_len: 2, auto_info: true, From 511d9d8a527d8c3dbce6ae1f9a0f5d7beaaf4b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 21 Oct 2022 13:26:00 +0900 Subject: [PATCH 097/151] cargo fmt --- helix-view/src/document.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ca07431b20e50..9a7febd2def26 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1372,7 +1372,7 @@ mod test { .to_string(); let expectation = std::fs::read_to_string(ref_path).unwrap(); assert_eq!(text[..], expectation[..]); - } + } }; ($name:ident, $label:expr) => { decode!($name, $label, $label); @@ -1389,11 +1389,11 @@ mod test { let ref_path = base_path.join(format!("{}_out_ref.txt", $label)); assert!(path.exists()); assert!(ref_path.exists()); - + let text = Rope::from_str(&std::fs::read_to_string(path).unwrap()); let mut buf: Vec = Vec::new(); helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap(); - + let expectation = std::fs::read(ref_path).unwrap(); assert_eq!(buf, expectation); } From 74a6a2282e79190baa59cd52907e014b31331819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 21 Oct 2022 13:27:05 +0900 Subject: [PATCH 098/151] Add undercurl styles to the default theme --- theme.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/theme.toml b/theme.toml index ab7c995a8c1f9..054093195a872 100644 --- a/theme.toml +++ b/theme.toml @@ -72,11 +72,10 @@ label = "honey" "ui.menu.selected" = { fg = "revolver", bg = "white" } "ui.menu.scroll" = { fg = "lavender", bg = "comet" } -diagnostic = { modifiers = ["underlined"] } -# "diagnostic.hint" = { fg = "revolver", bg = "lilac" } -# "diagnostic.info" = { fg = "revolver", bg = "lavender" } -# "diagnostic.warning" = { fg = "revolver", bg = "honey" } -# "diagnostic.error" = { fg = "revolver", bg = "apricot" } +"diagnostic.hint" = { underline = { color = "silver", style = "curl" } } +"diagnostic.info" = { underline = { color = "delta", style = "curl" } } +"diagnostic.warning" = { underline = { color = "lightning", style = "curl" } } +"diagnostic.error" = { underline = { color = "apricot", style = "curl" } } warning = "lightning" error = "apricot" From f486f34ebe0e21f0e109e24037e73182b2cf510a Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 21 Oct 2022 00:28:29 -0400 Subject: [PATCH 099/151] flush writes on force quit (#4397) When force quitting, we need to block on the pending writes to ensure that write commands succeed before exiting, and also to avoid a crash when all the views are gone before the auto format call returns from the LS. --- helix-term/src/commands/typed.rs | 1 + helix-term/tests/integration.rs | 2 +- helix-term/tests/test/auto_indent.rs | 2 +- helix-term/tests/test/auto_pairs.rs | 50 ++++++++++++++-------------- helix-term/tests/test/commands.rs | 2 +- helix-term/tests/test/movement.rs | 14 ++++---- helix-term/tests/test/prompt.rs | 2 +- helix-term/tests/test/write.rs | 14 ++++---- 8 files changed, 44 insertions(+), 43 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f154fd3897f61..394c073390cc4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -51,6 +51,7 @@ fn force_quit( ensure!(args.is_empty(), ":quit! takes no arguments"); + cx.block_try_flush_writes()?; cx.editor.close(view!(cx.editor).id); Ok(()) diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index e3754c436190b..a378af7a9b25f 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -11,7 +11,7 @@ mod test { use self::helpers::*; - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn hello_world() -> anyhow::Result<()> { test(("#[\n|]#", "ihello world", "hello world#[|\n]#")).await?; Ok(()) diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs index 5c093a5db4548..d5c220b7a1be2 100644 --- a/helix-term/tests/test/auto_indent.rs +++ b/helix-term/tests/test/auto_indent.rs @@ -1,6 +1,6 @@ use super::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn auto_indent_c() -> anyhow::Result<()> { test_with_config( Args { diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs index f2ab49c7f1781..e18c71195fb49 100644 --- a/helix-term/tests/test/auto_pairs.rs +++ b/helix-term/tests/test/auto_pairs.rs @@ -12,7 +12,7 @@ fn matching_pairs() -> impl Iterator { DEFAULT_PAIRS.iter().filter(|(open, close)| open == close) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_basic() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -26,7 +26,7 @@ async fn insert_basic() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> { // NOTE: these are multi-byte Unicode characters let pairs = hashmap!('„' => '“', '‚' => '‘', '「' => '」'); @@ -68,7 +68,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_after_word() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -91,7 +91,7 @@ async fn insert_after_word() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_before_word() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -105,7 +105,7 @@ async fn insert_before_word() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_before_word_selection() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -119,7 +119,7 @@ async fn insert_before_word_selection() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_before_word_selection_trailing_word() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -133,7 +133,7 @@ async fn insert_before_word_selection_trailing_word() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_closer_selection_trailing_word() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -147,7 +147,7 @@ async fn insert_closer_selection_trailing_word() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_before_eol() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -166,7 +166,7 @@ async fn insert_before_eol() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_auto_pairs_disabled() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test_with_config( @@ -191,7 +191,7 @@ async fn insert_auto_pairs_disabled() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_multi_range() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -210,7 +210,7 @@ async fn insert_multi_range() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_before_multi_code_point_graphemes() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -223,7 +223,7 @@ async fn insert_before_multi_code_point_graphemes() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_at_end_of_document() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(TestCase { @@ -248,7 +248,7 @@ async fn insert_at_end_of_document() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_close_inside_pair() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -272,7 +272,7 @@ async fn insert_close_inside_pair() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_close_inside_pair_multi() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -296,7 +296,7 @@ async fn insert_close_inside_pair_multi() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_nested_open_inside_pair() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -320,7 +320,7 @@ async fn insert_nested_open_inside_pair() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_nested_open_inside_pair_multi() -> anyhow::Result<()> { for outer_pair in DEFAULT_PAIRS { for inner_pair in DEFAULT_PAIRS { @@ -352,7 +352,7 @@ async fn insert_nested_open_inside_pair_multi() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_basic() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -371,7 +371,7 @@ async fn append_basic() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_multi_range() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -390,7 +390,7 @@ async fn append_multi_range() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_close_inside_pair() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -414,7 +414,7 @@ async fn append_close_inside_pair() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_close_inside_pair_multi() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( @@ -438,7 +438,7 @@ async fn append_close_inside_pair_multi() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_end_of_word() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -457,7 +457,7 @@ async fn append_end_of_word() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_middle_of_word() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -471,7 +471,7 @@ async fn append_middle_of_word() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_end_of_word_multi() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -490,7 +490,7 @@ async fn append_end_of_word_multi() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_inside_nested_pair() -> anyhow::Result<()> { for pair in differing_pairs() { test(( @@ -514,7 +514,7 @@ async fn append_inside_nested_pair() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn append_inside_nested_pair_multi() -> anyhow::Result<()> { for outer_pair in DEFAULT_PAIRS { for inner_pair in DEFAULT_PAIRS { diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 5238cc69b5f9d..e24ee3e08af69 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -94,7 +94,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_selection_duplication() -> anyhow::Result<()> { // Forward test(( diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 81c66e534c55f..fedf4b0e60e17 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -1,6 +1,6 @@ use super::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_mode_cursor_position() -> anyhow::Result<()> { test(TestCase { in_text: String::new(), @@ -19,7 +19,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { } /// Range direction is preserved when escaping insert mode to normal -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { test(("#[f|]#oo\n", "vll", "#[|foo]#\n")).await?; test(( @@ -66,7 +66,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { /// Ensure the very initial cursor in an opened file is the width of /// the first grapheme -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { let file = helpers::temp_file_with_contents(content)?; @@ -88,7 +88,7 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn cursor_position_append_eof() -> anyhow::Result<()> { // Selection is fowards test(( @@ -109,7 +109,7 @@ async fn cursor_position_append_eof() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::Result<()> { test_with_config( Args { @@ -141,7 +141,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow:: Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Result<()> { test_with_config( Args { @@ -173,7 +173,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> anyhow::Result<()> { // Note: the anchor stays put and the head moves back. test_with_config( diff --git a/helix-term/tests/test/prompt.rs b/helix-term/tests/test/prompt.rs index 62ec03f1ba61c..4f3bf76329dce 100644 --- a/helix-term/tests/test/prompt.rs +++ b/helix-term/tests/test/prompt.rs @@ -1,6 +1,6 @@ use super::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_history_completion() -> anyhow::Result<()> { test_key_sequence( &mut AppBuilder::new().build()?, diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 6aa51a31556bd..d0128edcaddf0 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -8,7 +8,7 @@ use helix_view::doc; use super::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() @@ -92,7 +92,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; let mut app = helpers::AppBuilder::new() @@ -133,7 +133,7 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; @@ -158,7 +158,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> { helpers::test_key_sequence_with_input_text( None, @@ -179,7 +179,7 @@ async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> { let mut file = tempfile::Builder::new().suffix(".rs").tempfile()?; @@ -203,7 +203,7 @@ async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_new_path() -> anyhow::Result<()> { let mut file1 = tempfile::NamedTempFile::new().unwrap(); let mut file2 = tempfile::NamedTempFile::new().unwrap(); @@ -249,7 +249,7 @@ async fn test_write_new_path() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_write_fail_new_path() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; From bad49ef2d095cca88a9e0bd39da329b86bda3d75 Mon Sep 17 00:00:00 2001 From: Nimrod Date: Fri, 21 Oct 2022 06:08:37 +0000 Subject: [PATCH 100/151] Fix unexpected behavior in delete_word_backward and delete_word_forward (#4392) --- helix-term/src/commands.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a8dafc33601b0..c5ee72d2d7389 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3238,8 +3238,8 @@ pub mod insert { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { - let cursor = Range::point(range.cursor(text)); - let next = movement::move_prev_word_start(text, cursor, count); + let anchor = movement::move_prev_word_start(text, range, count).from(); + let next = Range::new(anchor, range.cursor(text)); exclude_cursor(text, next, range) }); delete_selection_insert_mode(doc, view, &selection); @@ -3252,10 +3252,11 @@ pub mod insert { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_word_start(text, range, count)); + let selection = doc.selection(view.id).clone().transform(|range| { + let head = movement::move_next_word_end(text, range, count).to(); + Range::new(range.cursor(text), head) + }); + delete_selection_insert_mode(doc, view, &selection); lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic); From 5a848344a9b6f476922ea004e6a7f5b6d32ef768 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 21 Oct 2022 03:43:22 -0400 Subject: [PATCH 101/151] fix: write-all crash (#4384) When we do auto formatting, the code that takes the LSP's response and applies the changes to the document are just getting the currently focused view and giving that to the function, basically always assuming that the document that we're applying the change to is in focus, and not in a background view. This is usually fine for a single view, even if it's a buffer in the background, because it's still the same view and the selection will get updated accordingly for when you switch back to it. But it's obviously a problem for when there are multiple views, because if you don't have the target document in focus, it will ask the document to update the wrong view, hence the crash. The problem with this is picking which view to apply any selection change to. In the absence of any more data points on the views themselves, we simply pick the first view associated with the document we are saving. --- helix-term/src/commands.rs | 5 +++-- helix-term/src/commands/typed.rs | 35 ++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c5ee72d2d7389..e44b851e57cde 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2515,19 +2515,20 @@ fn insert_at_line_end(cx: &mut Context) { async fn make_format_callback( doc_id: DocumentId, doc_version: i32, + view_id: ViewId, format: impl Future> + Send + 'static, write: Option<(Option, bool)>, ) -> anyhow::Result { let format = format.await; let call: job::Callback = Callback::Editor(Box::new(move |editor| { - if !editor.documents.contains_key(&doc_id) { + if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) { return; } let scrolloff = editor.config().scrolloff; let doc = doc_mut!(editor, &doc_id); - let view = view_mut!(editor); + let view = view_mut!(editor, view_id); if let Ok(format) = format { if doc.version() == doc_version { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 394c073390cc4..7ea4c8018b0c7 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -271,7 +271,7 @@ fn write_impl( ) -> anyhow::Result<()> { let editor_auto_fmt = cx.editor.config().auto_format; let jobs = &mut cx.jobs; - let doc = doc_mut!(cx.editor); + let (view, doc) = current!(cx.editor); let path = path.map(AsRef::as_ref); let fmt = if editor_auto_fmt { @@ -279,6 +279,7 @@ fn write_impl( let callback = make_format_callback( doc.id(), doc.version(), + view.id, fmt, Some((path.map(Into::into), force)), ); @@ -344,9 +345,9 @@ fn format( return Ok(()); } - let doc = doc!(cx.editor); + let (view, doc) = current!(cx.editor); if let Some(format) = doc.format() { - let callback = make_format_callback(doc.id(), doc.version(), format, None); + let callback = make_format_callback(doc.id(), doc.version(), view.id, format, None); cx.jobs.callback(callback); } @@ -568,12 +569,13 @@ pub fn write_all_impl( let mut errors: Vec<&'static str> = Vec::new(); let auto_format = cx.editor.config().auto_format; let jobs = &mut cx.jobs; + let current_view = view!(cx.editor); // save all documents let saves: Vec<_> = cx .editor .documents - .values() + .values_mut() .filter_map(|doc| { if !doc.is_modified() { return None; @@ -585,10 +587,30 @@ pub fn write_all_impl( return None; } + // Look for a view to apply the formatting change to. If the document + // is in the current view, just use that. Otherwise, since we don't + // have any other metric available for better selection, just pick + // the first view arbitrarily so that we still commit the document + // state for undos. If somehow we have a document that has not been + // initialized with any view, initialize it with the current view. + let target_view = if doc.selections().contains_key(¤t_view.id) { + current_view.id + } else if let Some(view) = doc.selections().keys().next() { + *view + } else { + doc.ensure_view_init(current_view.id); + current_view.id + }; + let fmt = if auto_format { doc.auto_format().map(|fmt| { - let callback = - make_format_callback(doc.id(), doc.version(), fmt, Some((None, force))); + let callback = make_format_callback( + doc.id(), + doc.version(), + target_view, + fmt, + Some((None, force)), + ); jobs.add(Job::with_callback(callback).wait_before_exiting()); }) } else { @@ -598,6 +620,7 @@ pub fn write_all_impl( if fmt.is_none() { return Some(doc.id()); } + None }) .collect(); From 3aea33a4152e8ad1018b7d7019b2d2fde971eea4 Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sat, 15 Oct 2022 18:39:55 +0200 Subject: [PATCH 102/151] nit: move an allocation to happen after a `continue`, making sure it's not done for nothing --- helix-term/src/application.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8ee0802f20184..173c5d49a55b0 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -597,14 +597,14 @@ impl Application { // trigger textDocument/didOpen for docs that are already open for doc in docs { - let language_id = - doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); - let url = match doc.url() { Some(url) => url, None => continue, // skip documents with no path }; + let language_id = + doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); + tokio::spawn(language_server.text_document_did_open( url, doc.version(), From 34389e1d5472f458934b0af2a15192e2b8d83e1e Mon Sep 17 00:00:00 2001 From: "Alexis (Poliorcetics) Bourget" Date: Sat, 15 Oct 2022 19:16:17 +0200 Subject: [PATCH 103/151] nit: Do less allocations in `ui::menu::Item::label` implementations This complicates the code a little but it often divides by two the number of allocations done by the functions. LSP labels especially can easily be called dozens of time in a single menu popup, when listing references for example. --- helix-term/src/commands.rs | 25 +++++++------ helix-term/src/commands/lsp.rs | 66 ++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e44b851e57cde..468e9814d2ec1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -54,7 +54,7 @@ use crate::{ use crate::job::{self, Jobs}; use futures_util::StreamExt; -use std::{collections::HashMap, fmt, future::Future}; +use std::{collections::HashMap, fmt, fmt::Write, future::Future}; use std::{collections::HashSet, num::NonZeroUsize}; use std::{ @@ -2416,19 +2416,18 @@ impl ui::menu::Item for MappableCommand { type Data = ReverseKeymap; fn label(&self, keymap: &Self::Data) -> Spans { - // formats key bindings, multiple bindings are comma separated, - // individual key presses are joined with `+` let fmt_binding = |bindings: &Vec>| -> String { - bindings - .iter() - .map(|bind| { - bind.iter() - .map(|key| key.to_string()) - .collect::>() - .join("+") - }) - .collect::>() - .join(", ") + bindings.iter().fold(String::new(), |mut acc, bind| { + if !acc.is_empty() { + acc.push_str(", "); + } + bind.iter().fold(false, |needs_plus, key| { + write!(&mut acc, "{}{}", if needs_plus { "+" } else { "" }, key) + .expect("Writing to a string can only fail on an Out-Of-Memory error"); + true + }); + acc + }) }; match self { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 987fc4ce170ef..3c72cd2a54422 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -18,7 +18,9 @@ use crate::{ }, }; -use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, path::PathBuf, sync::Arc}; +use std::{ + borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Write, path::PathBuf, sync::Arc, +}; /// Gets the language server that is attached to a document, and /// if it's not active displays a status message. Using this macro @@ -43,23 +45,32 @@ impl ui::menu::Item for lsp::Location { type Data = PathBuf; fn label(&self, cwdir: &Self::Data) -> Spans { - let file: Cow<'_, str> = (self.uri.scheme() == "file") - .then(|| { - self.uri - .to_file_path() - .map(|path| { - // strip root prefix - path.strip_prefix(&cwdir) - .map(|path| path.to_path_buf()) - .unwrap_or(path) - }) - .map(|path| Cow::from(path.to_string_lossy().into_owned())) - .ok() - }) - .flatten() - .unwrap_or_else(|| self.uri.as_str().into()); - let line = self.range.start.line; - format!("{}:{}", file, line).into() + // The preallocation here will overallocate a few characters since it will account for the + // URL's scheme, which is not used most of the time since that scheme will be "file://". + // Those extra chars will be used to avoid allocating when writing the line number (in the + // common case where it has 5 digits or less, which should be enough for a cast majority + // of usages). + let mut res = String::with_capacity(self.uri.as_str().len()); + + if self.uri.scheme() == "file" { + // With the preallocation above and UTF-8 paths already, this closure will do one (1) + // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`. + let mut write_path_to_res = || -> Option<()> { + let path = self.uri.to_file_path().ok()?; + res.push_str(&path.strip_prefix(&cwdir).unwrap_or(&path).to_string_lossy()); + Some(()) + }; + write_path_to_res(); + } else { + // Never allocates since we declared the string with this capacity already. + res.push_str(self.uri.as_str()); + } + + // Most commonly, this will not allocate, especially on Unix systems where the root prefix + // is a simple `/` and not `C:\` (with whatever drive letter) + write!(&mut res, ":{}", self.range.start.line) + .expect("Will only failed if allocating fail"); + res.into() } } @@ -73,10 +84,8 @@ impl ui::menu::Item for lsp::SymbolInformation { } else { match self.location.uri.to_file_path() { Ok(path) => { - let relative_path = helix_core::path::get_relative_path(path.as_path()) - .to_string_lossy() - .into_owned(); - format!("{} ({})", &self.name, relative_path).into() + let get_relative_path = path::get_relative_path(path.as_path()); + format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into() } Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(), } @@ -115,24 +124,21 @@ impl ui::menu::Item for PickerDiagnostic { // remove background as it is distracting in the picker list style.bg = None; - let code = self + let code: Cow<'_, str> = self .diag .code .as_ref() .map(|c| match c { - NumberOrString::Number(n) => n.to_string(), - NumberOrString::String(s) => s.to_string(), + NumberOrString::Number(n) => n.to_string().into(), + NumberOrString::String(s) => s.as_str().into(), }) - .map(|code| format!(" ({})", code)) .unwrap_or_default(); let path = match format { DiagnosticsFormat::HideSourcePath => String::new(), DiagnosticsFormat::ShowSourcePath => { - let path = path::get_truncated_path(self.url.path()) - .to_string_lossy() - .into_owned(); - format!("{}: ", path) + let path = path::get_truncated_path(self.url.path()); + format!("{}: ", path.to_string_lossy()) } }; From 4b85aeb2b68c0aaf2338d3e0d0ad4daff13a7baf Mon Sep 17 00:00:00 2001 From: Christian Speich Date: Wed, 19 Oct 2022 10:13:39 +0200 Subject: [PATCH 104/151] grammar: Don't require lower-case (#4346) Currently we always lower-case the grammar name when loading it. While it is somewhat of an convention to name tree-sitter grammars in lowercase there is no rule to enforce it. This patch removes the lower-casing to allow all possible grammar names. Signed-off-by: Christian Speich --- helix-loader/src/grammar.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index eb1895a554c68..a92cadb64b626 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -67,7 +67,6 @@ pub fn get_language(name: &str) -> Result { #[cfg(not(target_arch = "wasm32"))] pub fn get_language(name: &str) -> Result { use libloading::{Library, Symbol}; - let name = name.to_ascii_lowercase(); let mut library_path = crate::runtime_dir().join("grammars").join(&name); library_path.set_extension(DYLIB_EXTENSION); From 79ef39ab3a41c32fcdcb660df6ee60aeb1eb80e6 Mon Sep 17 00:00:00 2001 From: Christian Speich Date: Wed, 19 Oct 2022 10:29:09 +0200 Subject: [PATCH 105/151] syntax: Don't force lower-case for filenames (#4346) Just like for grammars we currently force a lower-case of the name for some actions (like filesystem lookup). To make this consistent and less surprising for users, we remove this lower-casing here. Note: it is still the preferred way to name both language and grammar in lower-case Signed-off-by: Christian Speich --- helix-core/src/syntax.rs | 26 +++++++++++++++----------- xtask/src/querycheck.rs | 4 ++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f9a2ea5f7630f..21d19ce786355 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -361,14 +361,12 @@ pub fn read_query(language: &str, filename: &str) -> String { impl LanguageConfiguration { fn initialize_highlight(&self, scopes: &[String]) -> Option> { - let language = self.language_id.to_ascii_lowercase(); - - let highlights_query = read_query(&language, "highlights.scm"); + let highlights_query = read_query(&self.language_id, "highlights.scm"); // always highlight syntax errors // highlights_query += "\n(ERROR) @error"; - let injections_query = read_query(&language, "injections.scm"); - let locals_query = read_query(&language, "locals.scm"); + let injections_query = read_query(&self.language_id, "injections.scm"); + let locals_query = read_query(&self.language_id, "locals.scm"); if highlights_query.is_empty() { None @@ -432,14 +430,20 @@ impl LanguageConfiguration { } fn load_query(&self, kind: &str) -> Option { - let lang_name = self.language_id.to_ascii_lowercase(); - let query_text = read_query(&lang_name, kind); + let query_text = read_query(&self.language_id, kind); if query_text.is_empty() { return None; } let lang = self.highlight_config.get()?.as_ref()?.language; Query::new(lang, &query_text) - .map_err(|e| log::error!("Failed to parse {} queries for {}: {}", kind, lang_name, e)) + .map_err(|e| { + log::error!( + "Failed to parse {} queries for {}: {}", + kind, + self.language_id, + e + ) + }) .ok() } } @@ -2119,7 +2123,7 @@ mod test { ); let loader = Loader::new(Configuration { language: vec![] }); - let language = get_language("Rust").unwrap(); + let language = get_language("rust").unwrap(); let query = Query::new(language, query_str).unwrap(); let textobject = TextObjectQuery { query }; @@ -2179,7 +2183,7 @@ mod test { let loader = Loader::new(Configuration { language: vec![] }); - let language = get_language("Rust").unwrap(); + let language = get_language("rust").unwrap(); let config = HighlightConfiguration::new( language, &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm") @@ -2275,7 +2279,7 @@ mod test { let source = Rope::from_str(source); let loader = Loader::new(Configuration { language: vec![] }); - let language = get_language("Rust").unwrap(); + let language = get_language("rust").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); diff --git a/xtask/src/querycheck.rs b/xtask/src/querycheck.rs index 5595b8ec88fb2..7014c7d6f5d55 100644 --- a/xtask/src/querycheck.rs +++ b/xtask/src/querycheck.rs @@ -14,8 +14,8 @@ pub fn query_check() -> Result<(), DynError> { ]; for language in lang_config().language { - let language_name = language.language_id.to_ascii_lowercase(); - let grammar_name = language.grammar.unwrap_or(language.language_id); + let language_name = &language.language_id; + let grammar_name = language.grammar.as_ref().unwrap_or(language_name); for query_file in query_files { let language = get_language(&grammar_name); let query_text = read_query(&language_name, query_file); From 664064b3cca49479c3e893f4865e64cb587d2f61 Mon Sep 17 00:00:00 2001 From: Sora Date: Sat, 22 Oct 2022 00:32:29 +0200 Subject: [PATCH 106/151] Add textobjects.scm for zig (#4409) --- book/src/generated/lang-support.md | 2 +- runtime/queries/zig/textobjects.scm | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 runtime/queries/zig/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index c6edc66d402be..caf5d525b16b7 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -126,4 +126,4 @@ | wgsl | ✓ | | | `wgsl_analyzer` | | xit | ✓ | | | | | yaml | ✓ | | ✓ | `yaml-language-server` | -| zig | ✓ | | ✓ | `zls` | +| zig | ✓ | ✓ | ✓ | `zls` | diff --git a/runtime/queries/zig/textobjects.scm b/runtime/queries/zig/textobjects.scm new file mode 100644 index 0000000000000..67528943c9d75 --- /dev/null +++ b/runtime/queries/zig/textobjects.scm @@ -0,0 +1,23 @@ +(TopLevelDecl (FnProto) + (_) @function.inside) @function.around + +(TestDecl (_) @test.inside) @test.around + +; matches all of: struct, enum, union +; this unfortunately cannot be split up because +; of the way struct "container" types are defined +(TopLevelDecl (VarDecl (ErrorUnionExpr (SuffixExpr (ContainerDecl + (_) @class.inside))))) @class.around + +(TopLevelDecl (VarDecl (ErrorUnionExpr (SuffixExpr (ErrorSetDecl + (_) @class.inside))))) @class.around + +(ParamDeclList + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +[ + (doc_comment) + (line_comment) +] @comment.inside +(line_comment)+ @comment.around +(doc_comment)+ @comment.around From a449192ee9fb6ed59530b3c95e96d2211e6b7e5d Mon Sep 17 00:00:00 2001 From: PORTALSURFER <41680373+PORTALSURFER@users.noreply.github.com> Date: Sat, 22 Oct 2022 00:35:02 +0200 Subject: [PATCH 107/151] Added 2 new themes (#4367) --- runtime/themes/hex_steel.toml | 105 ++++++++++++++++++++++++++++++++++ runtime/themes/hex_toxic.toml | 30 ++++++++++ 2 files changed, 135 insertions(+) create mode 100644 runtime/themes/hex_steel.toml create mode 100644 runtime/themes/hex_toxic.toml diff --git a/runtime/themes/hex_steel.toml b/runtime/themes/hex_steel.toml new file mode 100644 index 0000000000000..06e91d01374e4 --- /dev/null +++ b/runtime/themes/hex_steel.toml @@ -0,0 +1,105 @@ +"comment" = { fg = "highlight_three" } +"comment.block.documentation" = { bg = "t4", modifiers = ["italic"] } + +"constant" = { fg = "t11" } +"function" = { fg = "t10" } +"function.method" = { fg = "t10" } +"function.macro" = { fg = "t7" } +"keyword.storage.modifier" = { fg = "t7" } +"keyword.control.import" = { fg = "t8" } +"keyword.control" = { fg = "t8" } +"keyword.function" = { fg = "t7" } +"keyword" = { fg = "t6" } +"operator" = { fg = "t8" } +"punctuation" = { fg = "t9" } +"string" = { fg = "t6", modifiers = ["italic"] } +"string.regexp" = { fg = "t6" } +"tag" = { fg = "t4" } +"type" = { fg = "t8", modifiers = ["bold"] } +"namespace" = { fg = "t6", modifiers = ["bold"] } +"variable" = { fg = "t4" } +"label" = { fg = "t4" } + +"diff.plus" = { fg = "t4" } +"diff.delta" = { fg = "t4" } +"diff.minus" = { fg = "t4" } + +"ui.cursor.insert" = { fg = "t2", bg = "highlight" } +"ui.cursor.select" = { fg = "t2", bg = "highlight_two" } +"ui.cursor" = { fg = "t1", bg = "highlight_three" } +"ui.cursor.match" = { fg = "highlight", bg = "t1", modifiers = ["bold"] } + +"ui.linenr" = { fg = "t3", bg = "t1" } +"ui.linenr.selected" = { fg = "highlight_three", bg = "t1" } +"ui.gutter" = { bg = "t1" } + +"ui.background" = { fg = "t4", bg = "t2" } +"ui.background.separator" = { fg = "t3" } +"ui.help" = { fg = "t4", bg = "t1" } +"ui.menu" = { fg = "t4", bg = "t1" } +"ui.menu.selected" = { fg = "highlight_three", bg = "t1" } +"ui.popup" = { fg = "t4", bg = "t1" } +"ui.window" = { fg = "t4" } + +"ui.selection.primary" = { bg = "selection" } +"ui.selection" = { bg = "selection" } + +"ui.cursorline.primary" = { bg = "t1" } + +"ui.statusline" = { fg = "t4", bg = "t1" } +"ui.statusline.inactive" = { fg = "t4", bg = "t1" } +"ui.statusline.normal" = { fg = "t3", bg = "t1" } +"ui.statusline.insert" = { fg = "t3", bg = "t1" } +"ui.statusline.select" = { fg = "highlight", bg = "t4" } + +"ui.text" = { fg = "t4" } +"ui.text.focus" = { fg = "highlight_three", modifiers = ["bold"] } +# +"ui.virtual.ruler" = { bg = "t1" } +"ui.virtual.indent-guide" = { fg = "t3" } +"ui.virtual.whitespace" = { fg = "t3" } + +"diagnostic" = { modifiers = ["underlined"] } +"error" = { fg = "error", modifiers = ["bold"] } +"warning" = { fg = "warning", modifiers = ["bold"] } +"info" = { fg = "info", modifiers = ["bold"] } +"hint" = { fg = "display", modifiers = ["bold"] } +"special" = { fg = "t7", modifiers = ["bold"] } + +"markup.heading" = { fg = "t4" } +"markup.list" = { fg = "t4" } +"markup.bold" = { fg = "t4" } +"markup.italic" = { fg = "t4" } +"markup.link.url" = { fg = "t4", modifiers = ["underlined"] } +"markup.link.text" = { fg = "t4" } +"markup.quote" = { fg = "t4" } +"markup.raw" = { fg = "t4" } + +[palette] +t1 = "#0f0b0b" +t2 = "#161010" +t3 = "#5b5555" +t4 = "#656869" +t5 = "#727b7c" +t6 = "#6e8789" +t7 = "#d85c60" +t8 = "#9bc1bb" +t9 = "#b5c5c5" +t10 = "#c0d0ce" +t11 = "#f78c5e" + +highlight = "#3f36f2" +highlight_two = "#f69c3c" +highlight_three = "#d4d987" + +selection = "#032d4a" + +black = "#000000" + +comment = "#396884" +comment_doc = "#234048" + +error = "#ff0900" +warning = "#ffbf00" +display = "#57ff89" +info = "#dad7d5" diff --git a/runtime/themes/hex_toxic.toml b/runtime/themes/hex_toxic.toml new file mode 100644 index 0000000000000..3cd878bef7ea1 --- /dev/null +++ b/runtime/themes/hex_toxic.toml @@ -0,0 +1,30 @@ +inherits = "hex_steel" + +[palette] +t1 = "#101719" +t2 = "#152432" +t3 = "#4b5968" +t4 = "#8792ab" +t5 = "#6f91bc" +t6 = "#8bb2b9" +t7 = "#eeac90" +t8 = "#b0bd9f" +t9 = "#b3ccd0" +t10 = "#b0d4d8" +t11 = "#ffbf52" + +highlight = "#ff2e5f" +highlight_two = "#0affa9" +highlight_three = "#d7ff52" + +black = "#000000" + +selection = "#290019" + +comment = "#396884" +comment_doc = "#234048" + +error = "#ff0900" +warning = "#ffbf00" +display = "#57ff89" +info = "#dad7d5" From 131d8392bbc3301ac4e0a392d92d08b08757b720 Mon Sep 17 00:00:00 2001 From: Mehedi Hasan <78611383+mehedir137@users.noreply.github.com> Date: Sat, 22 Oct 2022 04:37:10 +0600 Subject: [PATCH 108/151] Theme: Papercolor: Fixed cursorline background (#4317) --- runtime/themes/papercolor-dark.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/themes/papercolor-dark.toml b/runtime/themes/papercolor-dark.toml index 18a47292aa3c6..940aacfe21682 100644 --- a/runtime/themes/papercolor-dark.toml +++ b/runtime/themes/papercolor-dark.toml @@ -97,7 +97,7 @@ bright6="#00afaf" bright7="#5f8787" selection_foreground="#585858" selection_background="#8787AF" -cursorline_background="#d0d0d0" +cursorline_background="#303030" paper_bar_bg="#5F8787" black="#1c1c1c" red="#af005f" From 17daf6ac0a1a7ef4a44078ef11cc150a8fa41ff0 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 21 Oct 2022 19:34:15 -0500 Subject: [PATCH 109/151] Change syntax for suffix file-types configurations (#4414) The change in d801a6693c3d475b3942f705d3ef48d7966bdf65 to search for suffixes in `file-types` is too permissive: files like the tutor or `*.txt` files are now mistakenly interpreted as R or perl, respectively. This change changes the syntax for specifying a file-types entry that matches by suffix: ```toml file-types = [{ suffix = ".git/config" }] ``` And changes the file-type detection to first search for any non-suffix patterns and then search for suffixes only with the file-types entries marked explicitly as suffixes. --- book/src/languages.md | 28 ++++++++++- helix-core/src/syntax.rs | 102 ++++++++++++++++++++++++++++++++++----- languages.toml | 5 +- 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/book/src/languages.md b/book/src/languages.md index 9b90a2112b524..133e6447941cd 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -50,7 +50,7 @@ These configuration keys are available: | `name` | The name of the language | | `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.` or `text.` in case of markup languages | | `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | -| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. This attempts to match by exact file name (`.zshrc`), then by file extension (`toml`), then by path suffix (`.git/config`). | +| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. | | `shebangs` | The interpreters from the shebang line, for example `["sh", "bash"]` | | `roots` | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` | | `auto-format` | Whether to autoformat this language when saving | @@ -63,6 +63,32 @@ These configuration keys are available: | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `max-line-length` | Maximum line length. Used for the `:reflow` command | +### File-type detection and the `file-types` key + +Helix determines which language configuration to use with the `file-types` key +from the above section. `file-types` is a list of strings or tables, for +example: + +```toml +file-types = ["Makefile", "toml", { suffix = ".git/config" }] +``` + +When determining a language configuration to use, Helix searches the file-types +with the following priorities: + +1. Exact match: if the filename of a file is an exact match of a string in a + `file-types` list, that language wins. In the example above, `"Makefile"` + will match against `Makefile` files. +2. Extension: if there are no exact matches, any `file-types` string that + matches the file extension of a given file wins. In the example above, the + `"toml"` matches files like `Cargo.toml` or `languages.toml`. +3. Suffix: if there are still no matches, any values in `suffix` tables + are checked against the full path of the given file. In the example above, + the `{ suffix = ".git/config" }` would match against any `config` files + in `.git` directories. Note: `/` is used as the directory separator but is + replaced at runtime with the appropriate path separator for the operating + system, so this rule would match against `.git\config` files on Windows. + ### Language Server configuration The `language-server` field takes the following keys: diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 21d19ce786355..c17655a90bbdd 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -73,11 +73,11 @@ impl Default for Configuration { pub struct LanguageConfiguration { #[serde(rename = "name")] pub language_id: String, // c-sharp, rust - pub scope: String, // source.rust - pub file_types: Vec, // filename ends_with? + pub scope: String, // source.rust + pub file_types: Vec, // filename extension or ends_with? #[serde(default)] pub shebangs: Vec, // interpreter(s) associated with language - pub roots: Vec, // these indicate project roots <.git, Cargo.toml> + pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, pub max_line_length: Option, @@ -125,6 +125,78 @@ pub struct LanguageConfiguration { pub rulers: Option>, // if set, override editor's rulers } +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum FileType { + /// The extension of the file, either the `Path::extension` or the full + /// filename if the file does not have an extension. + Extension(String), + /// The suffix of a file. This is compared to a given file's absolute + /// path, so it can be used to detect files based on their directories. + Suffix(String), +} + +impl Serialize for FileType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + match self { + FileType::Extension(extension) => serializer.serialize_str(extension), + FileType::Suffix(suffix) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for FileType { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + struct FileTypeVisitor; + + impl<'de> serde::de::Visitor<'de> for FileTypeVisitor { + type Value = FileType; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string or table") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(FileType::Extension(value.to_string())) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + match map.next_entry::()? { + Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix( + suffix.replace('/', &std::path::MAIN_SEPARATOR.to_string()), + )), + Some((key, _value)) => Err(serde::de::Error::custom(format!( + "unknown key in `file-types` list: {}", + key + ))), + None => Err(serde::de::Error::custom( + "expected a `suffix` key in the `file-types` entry", + )), + } + } + } + + deserializer.deserialize_any(FileTypeVisitor) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -454,7 +526,8 @@ impl LanguageConfiguration { pub struct Loader { // highlight_names ? language_configs: Vec>, - language_config_ids_by_file_type: HashMap, // Vec + language_config_ids_by_extension: HashMap, // Vec + language_config_ids_by_suffix: HashMap, language_config_ids_by_shebang: HashMap, scopes: ArcSwap>, @@ -464,7 +537,8 @@ impl Loader { pub fn new(config: Configuration) -> Self { let mut loader = Self { language_configs: Vec::new(), - language_config_ids_by_file_type: HashMap::new(), + language_config_ids_by_extension: HashMap::new(), + language_config_ids_by_suffix: HashMap::new(), language_config_ids_by_shebang: HashMap::new(), scopes: ArcSwap::from_pointee(Vec::new()), }; @@ -475,10 +549,14 @@ impl Loader { for file_type in &config.file_types { // entry().or_insert(Vec::new).push(language_id); - let file_type = file_type.replace('/', &std::path::MAIN_SEPARATOR.to_string()); - loader - .language_config_ids_by_file_type - .insert(file_type, language_id); + match file_type { + FileType::Extension(extension) => loader + .language_config_ids_by_extension + .insert(extension.clone(), language_id), + FileType::Suffix(suffix) => loader + .language_config_ids_by_suffix + .insert(suffix.clone(), language_id), + }; } for shebang in &config.shebangs { loader @@ -498,14 +576,14 @@ impl Loader { let configuration_id = path .file_name() .and_then(|n| n.to_str()) - .and_then(|file_name| self.language_config_ids_by_file_type.get(file_name)) + .and_then(|file_name| self.language_config_ids_by_extension.get(file_name)) .or_else(|| { path.extension() .and_then(|extension| extension.to_str()) - .and_then(|extension| self.language_config_ids_by_file_type.get(extension)) + .and_then(|extension| self.language_config_ids_by_extension.get(extension)) }) .or_else(|| { - self.language_config_ids_by_file_type + self.language_config_ids_by_suffix .iter() .find_map(|(file_type, id)| { if path.to_str()?.ends_with(file_type) { diff --git a/languages.toml b/languages.toml index 5ad5c6e658494..a639ccadf2811 100644 --- a/languages.toml +++ b/languages.toml @@ -1053,8 +1053,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-regex", rev = "e1cf name = "git-config" scope = "source.gitconfig" roots = [] -# TODO: allow specifying file-types as a regex so we can read directory names (e.g. `.git/config`) -file-types = [".gitmodules", ".gitconfig", ".git/config", ".config/git/config"] +file-types = [".gitmodules", ".gitconfig", { suffix = ".git/config" }, { suffix = ".config/git/config" }] injection-regex = "git-config" comment-token = "#" indent = { tab-width = 4, unit = "\t" } @@ -1491,7 +1490,7 @@ source = { git = "https://github.com/bearcove/tree-sitter-meson", rev = "feea83b [[language]] name = "sshclientconfig" scope = "source.sshclientconfig" -file-types = [".ssh/config", "/etc/ssh/ssh_config"] +file-types = [{ suffix = ".ssh/config" }, { suffix = "/etc/ssh/ssh_config" }] roots = [] [[grammar]] From d7d0d5ffb79b6f2e09c6ab8af6e112c41e6f73e8 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 21 Oct 2022 20:04:50 -0500 Subject: [PATCH 110/151] lsp: Resolve completion items missing documentation on idle (#4406) Some language servers may not send the `documentation` field if it is expensive to compute. Clients can request the missing field with a completionItem/resolve request. In this change we use the idle-timeout event to ensure that the current completion item is resolved. --- helix-term/src/ui/completion.rs | 21 +++++++++++++++++++++ helix-term/src/ui/editor.rs | 13 +++++++++---- helix-term/src/ui/menu.rs | 8 ++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 7348dcf444316..a21767f944f86 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -295,6 +295,27 @@ impl Completion { pub fn is_empty(&self) -> bool { self.popup.contents().is_empty() } + + pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool { + // > If computing full completion items is expensive, servers can additionally provide a + // > handler for the completion item resolve request. ... + // > A typical use case is for example: the `textDocument/completion` request doesn't fill + // > in the `documentation` property for returned completion items since it is expensive + // > to compute. When the item is selected in the user interface then a + // > 'completionItem/resolve' request is sent with the selected completion item as a parameter. + // > The returned completion item should have the documentation property filled in. + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion + match self.popup.contents_mut().selection_mut() { + Some(item) if item.documentation.is_none() => { + let doc = doc!(cx.editor); + if let Some(resolved_item) = Self::resolve_completion_item(doc, item.clone()) { + *item = resolved_item; + } + true + } + _ => false, + } + } } impl Component for Completion { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 43b1e7a073034..b1bb02c756a8e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1097,10 +1097,15 @@ impl EditorView { } pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult { - if self.completion.is_some() - || cx.editor.mode != Mode::Insert - || !cx.editor.config().auto_completion - { + if let Some(completion) = &mut self.completion { + return if completion.ensure_item_resolved(cx) { + EventResult::Consumed(None) + } else { + EventResult::Ignored(None) + }; + } + + if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion { return EventResult::Ignored(None); } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 4b1155e3e75c9..75769b905b2af 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -206,6 +206,14 @@ impl Menu { }) } + pub fn selection_mut(&mut self) -> Option<&mut T> { + self.cursor.and_then(|cursor| { + self.matches + .get(cursor) + .map(|(index, _score)| &mut self.options[*index]) + }) + } + pub fn is_empty(&self) -> bool { self.matches.is_empty() } From ce469abfe2e99b5156c948bff27aaec3fbff7a6e Mon Sep 17 00:00:00 2001 From: "Mr. E" <2804556+etienne-k@users.noreply.github.com> Date: Sat, 22 Oct 2022 21:25:41 +0200 Subject: [PATCH 111/151] feat(themes): add mode-specific styles to the tokyonight themes (#4415) Co-authored-by: SoraTenshi --- runtime/themes/tokyonight.toml | 3 + runtime/themes/tokyonight_storm.toml | 82 +--------------------------- 2 files changed, 4 insertions(+), 81 deletions(-) diff --git a/runtime/themes/tokyonight.toml b/runtime/themes/tokyonight.toml index 4f1ea1cfe26fa..4847d92caaa65 100644 --- a/runtime/themes/tokyonight.toml +++ b/runtime/themes/tokyonight.toml @@ -41,6 +41,9 @@ "ui.selection.primary" = { bg = "background_highlight" } "ui.statusline" = { fg = "foreground", bg = "background_menu" } "ui.statusline.inactive" = { fg = "foreground_gutter", bg = "background_menu" } +"ui.statusline.normal" = { fg = "black", bg = "blue" } +"ui.statusline.insert" = { fg = "black", bg = "green" } +"ui.statusline.select" = { fg = "black", bg = "magenta" } "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "cyan" } "ui.virtual.ruler" = { bg = "foreground_gutter" } diff --git a/runtime/themes/tokyonight_storm.toml b/runtime/themes/tokyonight_storm.toml index c47ac54bad26c..e82c43409fe60 100644 --- a/runtime/themes/tokyonight_storm.toml +++ b/runtime/themes/tokyonight_storm.toml @@ -1,88 +1,8 @@ # Author: Paul Graydon -"comment" = { fg = "comment", modifiers = ["italic"] } -"constant" = { fg = "orange" } -"constant.character.escape" = { fg = "magenta" } -"function" = { fg = "blue", modifiers = ["italic"] } -"function.macro" = { fg = "cyan" } -"keyword" = { fg = "cyan", modifiers = ["italic"] } -"keyword.control" = { fg = "magenta" } -"keyword.control.import" = { fg = "cyan" } -"keyword.operator" = { fg = "turquoise" } -"keyword.function" = { fg = "magenta", modifiers = ["italic"] } -"operator" = { fg = "turquoise" } -"punctuation" = { fg = "turquoise" } -"string" = { fg = "light-green" } -"string.regexp" = { fg = "light-blue" } -"tag" = { fg = "red" } -"type" = { fg = "teal" } -"namespace" = { fg = "blue" } -"variable" = { fg = "white" } -"variable.builtin" = { fg = "red" } -"variable.other.member" = { fg = "green" } -"variable.parameter" = { fg = "yellow", modifiers = ["italic"] } - -"diff.plus" = { fg = "green" } -"diff.delta" = { fg = "orange" } -"diff.minus" = { fg = "red" } - -"ui.background" = { fg = "foreground", bg = "background" } -"ui.cursor" = { modifiers = ["reversed"] } -"ui.cursor.match" = { fg = "orange", modifiers = ["bold"] } -"ui.cursor.primary" = { modifiers = ["reversed"] } -"ui.cursorline.primary" = { bg = "background_menu" } -"ui.help" = { fg = "foreground", bg = "background_menu" } -"ui.linenr" = { fg = "foreground_gutter" } -"ui.linenr.selected" = { fg = "foreground" } -"ui.menu" = { fg = "foreground", bg = "background_menu" } -"ui.menu.selected" = { bg = "background_highlight" } -"ui.popup" = { fg = "foreground", bg = "background_menu" } -"ui.selection" = { bg = "background_highlight" } -"ui.selection.primary" = { bg = "background_highlight" } -"ui.statusline" = { fg = "foreground", bg = "background_menu" } -"ui.statusline.inactive" = { fg = "foreground_gutter", bg = "background_menu" } -"ui.text" = { fg = "foreground" } -"ui.text.focus" = { fg = "cyan" } -"ui.virtual.ruler" = { bg = "foreground_gutter" } -"ui.virtual.whitespace" = { fg = "foreground_gutter" } -"ui.window" = { fg = "black" } - -"error" = { fg = "red" } -"warning" = { fg = "yellow" } -"info" = { fg = "blue" } -"hint" = { fg = "teal" } -"diagnostic" = { modifiers = ["underlined"] } -"special" = { fg = "orange" } - -"markup.heading" = { fg = "cyan", modifiers = ["bold"] } -"markup.list" = { fg = "cyan" } -"markup.bold" = { fg = "orange", modifiers = ["bold"] } -"markup.italic" = { fg = "yellow", modifiers = ["italic"] } -"markup.link.url" = { fg = "green" } -"markup.link.text" = { fg = "light-gray" } -"markup.quote" = { fg = "yellow", modifiers = ["italic"] } -"markup.raw" = { fg = "cyan" } +inherits = "tokyonight" [palette] -red = "#f7768e" -orange = "#ff9e64" -yellow = "#e0af68" -light-green = "#9ece6a" -green = "#73daca" -turquoise = "#89ddff" -light-cyan = "#b4f9f8" -teal = "#2ac3de" -cyan = "#7dcfff" -blue = "#7aa2f7" -magenta = "#bb9af7" -white = "#c0caf5" -light-gray = "#9aa5ce" -parameters = "#cfc9c2" -comment = "#565f89" -black = "#414868" -foreground = "#a9b1d6" -foreground_highlight = "#c0caf5" -foreground_gutter = "#3b4261" background = "#24283b" background_highlight = "#373d5a" background_menu = "#1f2335" From 801984c7fc2cd3f4072f1bdf42c5fbfe1a634348 Mon Sep 17 00:00:00 2001 From: Mike Trinkala Date: Sun, 23 Oct 2022 16:02:58 -0700 Subject: [PATCH 112/151] Update textwrap to 0.16.0 (#4437) --- Cargo.lock | 29 +++++++++++++++++++++++++---- helix-core/Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec48c596449f6..878f4fa4acd2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -383,6 +394,15 @@ dependencies = [ "memmap2", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "helix-core" version = "0.6.0" @@ -1112,9 +1132,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" dependencies = [ "smawk", "unicode-linebreak", @@ -1264,10 +1284,11 @@ checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unicode-linebreak" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" dependencies = [ + "hashbrown", "regex", ] diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 4eaadd1e43711..7585c347607cd 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -43,7 +43,7 @@ encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } etcetera = "0.4" -textwrap = "0.15.1" +textwrap = "0.16.0" [dev-dependencies] quickcheck = { version = "1", default-features = false } From d8ffa02255b99f94475cfaa7f456c6ae991dda25 Mon Sep 17 00:00:00 2001 From: Owen Lynch Date: Mon, 24 Oct 2022 18:44:36 -0400 Subject: [PATCH 113/151] Update scala treesitter grammar (#4353) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index a639ccadf2811..6f58112a825a0 100644 --- a/languages.toml +++ b/languages.toml @@ -982,7 +982,7 @@ language-server = { command = "metals" } [[grammar]] name = "scala" -source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "0a3dd53a7fc4b352a538397d054380aaa28be54c" } +source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "140c96cf398693189d4e50f76d19ddfcd8a018f8" } [[language]] name = "dockerfile" From 45da569a4c2db545ffae041eab9fbc66bde44ed3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:14:05 -0500 Subject: [PATCH 114/151] build(deps): bump anyhow from 1.0.65 to 1.0.66 (#4459) 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 878f4fa4acd2e..7b2ea3782baa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,9 +33,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "arc-swap" From 6ea2c541528a2d4ae3562f45e3e6206cca96b6c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:14:27 -0500 Subject: [PATCH 115/151] build(deps): bump futures-util from 0.3.24 to 0.3.25 (#4460) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b2ea3782baa8..8440905126d31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,9 +288,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" @@ -305,15 +305,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-core", "futures-task", From 093842988b6c7fcd15dc90f9bf012a2fa6005de4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:18:41 -0500 Subject: [PATCH 116/151] build(deps): bump termini from 0.1.2 to 0.1.4 (#4461) 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 8440905126d31..96682aa3649eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,9 +1123,9 @@ dependencies = [ [[package]] name = "termini" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "394766021ef3dae8077f080518cdf5360831990f77f5708d5e3594c9b3efa2f9" +checksum = "8c0f7ecb9c2a380d2686a747e4fc574043712326e8d39fbd220ab3bd29768a12" dependencies = [ "dirs-next", ] From 02385599e1e47ec46f5cfbba8b454378d5f19f41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:18:50 -0500 Subject: [PATCH 117/151] build(deps): bump serde_json from 1.0.86 to 1.0.87 (#4462) 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 96682aa3649eb..3581399347ca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -959,9 +959,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "itoa", "ryu", From 6aacdaaf9f3e315b99aba8bae6d19c6dc0bae831 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:35:12 -0500 Subject: [PATCH 118/151] build(deps): bump futures-executor from 0.3.24 to 0.3.25 (#4464) 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 3581399347ca0..283fffe2beebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,9 +294,9 @@ checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", From 001e4e304b6177148b8a74a222e0d02b91f53a11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:37:25 -0500 Subject: [PATCH 119/151] build(deps): bump serde from 1.0.145 to 1.0.147 (#4463) 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 283fffe2beebe..163b3ad48bb79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -939,18 +939,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", From c47ca331374b28a70c8bb9fd0fe991c478c696a1 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 25 Oct 2022 07:03:35 -0500 Subject: [PATCH 120/151] Render diagnostics in the file picker preview (#4324) This is mostly for the sake of the diagnostics pickers: without rendering the diagnostic styles, it's hard to tell where the entries in the picker are pointing to. --- helix-term/src/ui/picker.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c7149c6139991..2505f21972e41 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -248,8 +248,14 @@ impl Component for FilePicker { let offset = Position::new(first_line, 0); - let highlights = + let mut highlights = EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme); + for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) { + if spans.is_empty() { + continue; + } + highlights = Box::new(helix_core::syntax::merge(highlights, spans)); + } EditorView::render_text_highlights( doc, offset, From 9fae4b81189e9ba02718595a09840535f6fb558b Mon Sep 17 00:00:00 2001 From: GabrielDertoni Date: Sun, 9 Oct 2022 00:23:02 -0300 Subject: [PATCH 121/151] fix: terminal freezing on `shell_insert_output` This bug occurs on `shell_insert_output` and `shell_append_output` commands. The previous implementation would create a child process using the Rust stdlib's `Command` builder. However, when nothing should be piped in from the editor, the default value for `stdin` would be used. According to the Rust stdlib documentation that is `Stdio::inherit` which will make the child process inherit the parent process' stdin. This would cause the terminal to freeze. This change will set the child process' stdin to `Stdio::null` whenever it doesn't pipe it. In the `if` statement where this change was made there was an extra condition for windows that I am not sure if would require some special treatment. --- helix-term/src/commands.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 468e9814d2ec1..69870a279f3ce 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4665,6 +4665,8 @@ fn shell_impl( if input.is_some() || cfg!(windows) { process.stdin(Stdio::piped()); + } else { + process.stdin(Stdio::null()); } let mut process = match process.spawn() { From 65edf9c19880fe4fd068db6d86c4c2830c8a91f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zabielski?= Date: Wed, 26 Oct 2022 04:15:46 +0200 Subject: [PATCH 122/151] fix: repeating repeat operator (#4450) --- helix-term/src/ui/editor.rs | 61 +++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b1bb02c756a8e..72c9d15e6d0a3 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -25,7 +25,7 @@ use helix_view::{ keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{borrow::Cow, cmp::min, path::PathBuf}; +use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf}; use tui::buffer::Buffer as Surface; @@ -1009,37 +1009,40 @@ impl EditorView { } // special handling for repeat operator (key!('.'), _) if self.keymaps.pending().is_empty() => { - // first execute whatever put us into insert mode - self.last_insert.0.execute(cxt); - // then replay the inputs - for key in self.last_insert.1.clone() { - match key { - InsertEvent::Key(key) => self.insert_mode(cxt, key), - InsertEvent::CompletionApply(compl) => { - let (view, doc) = current!(cxt.editor); - - doc.restore(view); - - let text = doc.text().slice(..); - let cursor = doc.selection(view.id).primary().cursor(text); - - let shift_position = - |pos: usize| -> usize { pos + cursor - compl.trigger_offset }; - - let tx = Transaction::change( - doc.text(), - compl.changes.iter().cloned().map(|(start, end, t)| { - (shift_position(start), shift_position(end), t) - }), - ); - apply_transaction(&tx, doc, view); - } - InsertEvent::TriggerCompletion => { - let (_, doc) = current!(cxt.editor); - doc.savepoint(); + for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) { + // first execute whatever put us into insert mode + self.last_insert.0.execute(cxt); + // then replay the inputs + for key in self.last_insert.1.clone() { + match key { + InsertEvent::Key(key) => self.insert_mode(cxt, key), + InsertEvent::CompletionApply(compl) => { + let (view, doc) = current!(cxt.editor); + + doc.restore(view); + + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + + let shift_position = + |pos: usize| -> usize { pos + cursor - compl.trigger_offset }; + + let tx = Transaction::change( + doc.text(), + compl.changes.iter().cloned().map(|(start, end, t)| { + (shift_position(start), shift_position(end), t) + }), + ); + apply_transaction(&tx, doc, view); + } + InsertEvent::TriggerCompletion => { + let (_, doc) = current!(cxt.editor); + doc.savepoint(); + } } } } + cxt.editor.count = None; } _ => { // set the count From ba9e50e93b43f102de352ead57c28d86c34a7c73 Mon Sep 17 00:00:00 2001 From: Gaurav Tyagi Date: Wed, 26 Oct 2022 08:28:49 +0530 Subject: [PATCH 123/151] Add `:update` that will write the changes if the file has been modified. (#4426) * add command update that will write the changes if file hasn been modified * add docs * update the docs --- book/src/generated/typable-cmd.md | 1 + helix-term/src/commands/typed.rs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 05a0985b41f87..49e7e7780a661 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -44,6 +44,7 @@ | `:show-directory`, `:pwd` | Show the current working directory. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:reload` | Discard changes and reload from the source file. | +| `:update` | Write changes only if the file has been modified. | | `:lsp-restart` | Restarts the Language Server that is in use by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 7ea4c8018b0c7..9b79c3e6c589c 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1029,6 +1029,24 @@ fn reload( }) } +/// Update the [`Document`] if it has been modified. +fn update( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let (_view, doc) = current!(cx.editor); + if doc.is_modified() { + write(cx, args, event) + } else { + Ok(()) + } +} + fn lsp_restart( cx: &mut compositor::Context, _args: &[Cow], @@ -1957,6 +1975,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: reload, completer: None, }, + TypableCommand { + name: "update", + aliases: &[], + doc: "Write changes only if the file has been modified.", + fun: update, + completer: None, + }, TypableCommand { name: "lsp-restart", aliases: &[], From ac0fe29867012ba0840ee7c8893b41d976d5ab38 Mon Sep 17 00:00:00 2001 From: "James O. D. Hunt" Date: Wed, 26 Oct 2022 03:59:50 +0100 Subject: [PATCH 124/151] commands: Make no arg ':theme' show name (#3740) Most commands that accept an argument show their current value if no argument is specified. The `:theme` command previously displayed an error message in the status bar if not provided with an argument: ``` Theme name not provided ``` It now shows the current theme name in the status bar if no argument is specified. Signed-off-by: James O. D. Hunt Signed-off-by: James O. D. Hunt --- book/src/generated/typable-cmd.md | 2 +- helix-term/src/commands/typed.rs | 25 ++++++++++++++--------- helix-view/src/theme.rs | 34 +++++++++++++++++++------------ 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 49e7e7780a661..f858ba7255dfa 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -28,7 +28,7 @@ | `:quit-all!`, `:qa!` | Force close all views ignoring unsaved changes. | | `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). | -| `:theme` | Change the editor theme. | +| `:theme` | Change the editor theme (show current theme if no name specified). | | `:clipboard-yank` | Yank main selection into system clipboard. | | `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | | `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 9b79c3e6c589c..0cf75ada91e70 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -772,16 +772,21 @@ fn theme( }; } PromptEvent::Validate => { - let theme_name = args.first().with_context(|| "Theme name not provided")?; - let theme = cx - .editor - .theme_loader - .load(theme_name) - .with_context(|| "Theme does not exist")?; - if !(true_color || theme.is_16_color()) { - bail!("Unsupported theme: theme requires true color support"); + if let Some(theme_name) = args.first() { + let theme = cx + .editor + .theme_loader + .load(theme_name) + .with_context(|| "Theme does not exist")?; + if !(true_color || theme.is_16_color()) { + bail!("Unsupported theme: theme requires true color support"); + } + cx.editor.set_theme(theme); + } else { + let name = cx.editor.theme.name().to_string(); + + cx.editor.set_status(name); } - cx.editor.set_theme(theme); } }; @@ -1866,7 +1871,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "theme", aliases: &[], - doc: "Change the editor theme.", + doc: "Change the editor theme (show current theme if no name specified).", fun: theme, completer: Some(completers::theme), }, diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 302844b7b7c4d..f1219ec5dc170 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -14,19 +14,14 @@ use toml::{map::Map, Value}; use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; -pub static DEFAULT_THEME: Lazy = Lazy::new(|| { - // let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml")) - // .expect("Failed to parse default theme"); - // Theme::from(raw_theme) - - toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") +pub static DEFAULT_THEME: Lazy = Lazy::new(|| Theme { + name: "default".into(), + ..toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") }); -pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| { - // let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml")) - // .expect("Failed to parse base 16 default theme"); - // Theme::from(raw_theme) - toml::from_slice(include_bytes!("../../base16_theme.toml")) +pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| Theme { + name: "base16_theme".into(), + ..toml::from_slice(include_bytes!("../../base16_theme.toml")) .expect("Failed to parse base 16 default theme") }); @@ -53,7 +48,12 @@ impl Loader { return Ok(self.base16_default()); } - self.load_theme(name, name, false).map(Theme::from) + let theme = self.load_theme(name, name, false).map(Theme::from)?; + + Ok(Theme { + name: name.into(), + ..theme + }) } // load the theme and its parent recursively and merge them @@ -180,8 +180,10 @@ impl Loader { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Theme { + name: String, + // UI styles are stored in a HashMap styles: HashMap, // tree-sitter highlight styles are stored in a Vec to optimize lookups @@ -200,6 +202,7 @@ impl From for Theme { styles, scopes, highlights, + ..Default::default() } } } @@ -217,6 +220,7 @@ impl<'de> Deserialize<'de> for Theme { styles, scopes, highlights, + ..Default::default() }) } } @@ -266,6 +270,10 @@ impl Theme { self.highlights[index] } + pub fn name(&self) -> &str { + &self.name + } + pub fn get(&self, scope: &str) -> Style { self.try_get(scope).unwrap_or_default() } From c4d7cde6c8d7f5ca3a9cabd50bdfbda9c3f7bb15 Mon Sep 17 00:00:00 2001 From: A-Walrus <58790821+A-Walrus@users.noreply.github.com> Date: Wed, 26 Oct 2022 06:00:13 +0300 Subject: [PATCH 125/151] Allow the area to be bigger than u16 (width and height remain u16) (#4318) Now the editor can fill **very** large terminals. Changed/removed tests which check the truncating behaviour. --- helix-tui/src/buffer.rs | 14 +++++------ helix-tui/tests/terminal.rs | 6 ++--- helix-view/src/graphics.rs | 49 ++++--------------------------------- 3 files changed, 15 insertions(+), 54 deletions(-) diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 424e6d321bd91..23ba43f1b2f1c 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -137,7 +137,7 @@ impl Buffer { /// Returns a Buffer with all cells initialized with the attributes of the given Cell pub fn filled(area: Rect, cell: &Cell) -> Buffer { - let size = area.area() as usize; + let size = area.area(); let mut content = Vec::with_capacity(size); for _ in 0..size { content.push(cell.clone()); @@ -239,7 +239,7 @@ impl Buffer { y, self.area ); - ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize + ((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize) } /// Returns the index in the Vec for the given global (x, y) coordinates, @@ -278,8 +278,8 @@ impl Buffer { self.content.len() ); ( - self.area.x + i as u16 % self.area.width, - self.area.y + i as u16 / self.area.width, + (self.area.x as usize + (i % self.area.width as usize)) as u16, + (self.area.y as usize + (i / self.area.width as usize)) as u16, ) } @@ -480,7 +480,7 @@ impl Buffer { /// Resize the buffer so that the mapped area matches the given area and that the buffer /// length is equal to area.width * area.height pub fn resize(&mut self, area: Rect) { - let length = area.area() as usize; + let length = area.area(); if self.content.len() > length { self.content.truncate(length); } else { @@ -587,8 +587,8 @@ impl Buffer { let mut to_skip: usize = 0; for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() { if (current != previous || invalidated > 0) && to_skip == 0 { - let x = i as u16 % width; - let y = i as u16 / width; + let x = (i % width as usize) as u16; + let y = (i / width as usize) as u16; updates.push((x, y, &next_buffer[i])); } diff --git a/helix-tui/tests/terminal.rs b/helix-tui/tests/terminal.rs index 3dd3b0b0dbd4f..2824c9f241a4b 100644 --- a/helix-tui/tests/terminal.rs +++ b/helix-tui/tests/terminal.rs @@ -4,12 +4,12 @@ use helix_tui::{ }; #[test] -fn terminal_buffer_size_should_be_limited() { +fn terminal_buffer_size_should_not_be_limited() { let backend = TestBackend::new(400, 400); let terminal = Terminal::new(backend).unwrap(); let size = terminal.backend().size().unwrap(); - assert_eq!(size.width, 255); - assert_eq!(size.height, 255); + assert_eq!(size.width, 400); + assert_eq!(size.height, 400); } // #[test] diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 4374a5371442c..cbae873a42f98 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -95,31 +95,19 @@ pub struct Rect { } impl Rect { - /// Creates a new rect, with width and height limited to keep the area under max u16. - /// If clipped, aspect ratio will be preserved. + /// Creates a new rect, with width and height pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect { - let max_area = u16::max_value(); - let (clipped_width, clipped_height) = - if u32::from(width) * u32::from(height) > u32::from(max_area) { - let aspect_ratio = f64::from(width) / f64::from(height); - let max_area_f = f64::from(max_area); - let height_f = (max_area_f / aspect_ratio).sqrt(); - let width_f = height_f * aspect_ratio; - (width_f as u16, height_f as u16) - } else { - (width, height) - }; Rect { x, y, - width: clipped_width, - height: clipped_height, + width, + height, } } #[inline] - pub fn area(self) -> u16 { - self.width * self.height + pub fn area(self) -> usize { + (self.width as usize) * (self.height as usize) } #[inline] @@ -630,33 +618,6 @@ impl Style { mod tests { use super::*; - #[test] - fn test_rect_size_truncation() { - for width in 256u16..300u16 { - for height in 256u16..300u16 { - let rect = Rect::new(0, 0, width, height); - rect.area(); // Should not panic. - assert!(rect.width < width || rect.height < height); - // The target dimensions are rounded down so the math will not be too precise - // but let's make sure the ratios don't diverge crazily. - assert!( - (f64::from(rect.width) / f64::from(rect.height) - - f64::from(width) / f64::from(height)) - .abs() - < 1.0 - ) - } - } - - // One dimension below 255, one above. Area above max u16. - let width = 900; - let height = 100; - let rect = Rect::new(0, 0, width, height); - assert_ne!(rect.width, 900); - assert_ne!(rect.height, 100); - assert!(rect.width < width || rect.height < height); - } - #[test] fn test_rect_size_preservation() { for width in 0..256u16 { From 27217bb435fa0242b5cf43a23d3d13c739866b9f Mon Sep 17 00:00:00 2001 From: Matthias Deiml Date: Thu, 27 Oct 2022 15:39:22 +0200 Subject: [PATCH 126/151] Update tree-sitter markdown grammar (#4483) --- languages.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index 6f58112a825a0..cc8f155bdcde3 100644 --- a/languages.toml +++ b/languages.toml @@ -943,7 +943,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "markdown" -source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "d5740f0fe4b8e4603f2229df107c5c9ef5eec389", subpath = "tree-sitter-markdown" } +source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "a7de4be29783a6e25f3240c90afea52f2417faa3", subpath = "tree-sitter-markdown" } [[language]] name = "markdown.inline" @@ -955,7 +955,7 @@ grammar = "markdown_inline" [[grammar]] name = "markdown_inline" -source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "d5740f0fe4b8e4603f2229df107c5c9ef5eec389", subpath = "tree-sitter-markdown-inline" } +source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "a7de4be29783a6e25f3240c90afea52f2417faa3", subpath = "tree-sitter-markdown-inline" } [[language]] name = "dart" From b1ffbbd49fd54525c55252162b5897d4ad4e990d Mon Sep 17 00:00:00 2001 From: Matthias Deiml Date: Thu, 27 Oct 2022 15:40:47 +0200 Subject: [PATCH 127/151] Include unnamed children for html injected into inline markdown (#4478) --- runtime/queries/markdown.inline/injections.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/queries/markdown.inline/injections.scm b/runtime/queries/markdown.inline/injections.scm index c137a8222a48a..2dd149d9062a9 100644 --- a/runtime/queries/markdown.inline/injections.scm +++ b/runtime/queries/markdown.inline/injections.scm @@ -1,2 +1,2 @@ -((html_tag) @injection.content (#set! injection.language "html")) +((html_tag) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children)) From b4a3dd8f89d6b3c1c973be2dfb2121c4b6cb501f Mon Sep 17 00:00:00 2001 From: lazytanuki <43273245+lazytanuki@users.noreply.github.com> Date: Thu, 27 Oct 2022 16:16:55 +0200 Subject: [PATCH 128/151] feat(lsp): LSP preselected items appear first in completion menu (#4480) * feat(lsp): LSP preselected items appear first in completion menu * fix: shorter diff --- helix-term/src/ui/completion.rs | 6 +++++- helix-term/src/ui/menu.rs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index a21767f944f86..de7c3232bb3d6 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -92,11 +92,15 @@ impl Completion { pub fn new( editor: &Editor, - items: Vec, + mut items: Vec, offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, ) -> Self { + // Sort completion items according to their preselect status (given by the LSP server) + items.sort_by_key(|item| !item.preselect.unwrap_or(false)); + + // Then create the menu let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 75769b905b2af..99c2473d6f40a 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -108,7 +108,8 @@ impl Menu { .map(|score| (index, score)) }), ); - self.matches.sort_unstable_by_key(|(_, score)| -score); + // Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority + self.matches.sort_by_key(|(_, score)| -score); // reset cursor position self.cursor = None; From de5b100556ce4d9a2f739bd54904246f97dc6863 Mon Sep 17 00:00:00 2001 From: Sora Date: Fri, 28 Oct 2022 03:19:01 +0200 Subject: [PATCH 129/151] Add the debugger for Zig (#4492) --- languages.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/languages.toml b/languages.toml index cc8f155bdcde3..0a87fad243e60 100644 --- a/languages.toml +++ b/languages.toml @@ -767,6 +767,29 @@ language-server = { command = "zls" } indent = { tab-width = 4, unit = " " } formatter = { command = "zig" , args = ["fmt", "--stdin"] } +[language.debugger] +name = "lldb-vscode" +transport = "stdio" +command = "lldb-vscode" + +[[language.debugger.templates]] +name = "binary" +request = "launch" +completion = [ { name = "binary", completion = "filename" } ] +args = { console = "internalConsole", program = "{0}" } + +[[language.debugger.templates]] +name = "attach" +request = "attach" +completion = [ "pid" ] +args = { console = "internalConsole", pid = "{0}" } + +[[language.debugger.templates]] +name = "gdbserver attach" +request = "attach" +completion = [ { name = "lldb connect url", default = "connect://localhost:3333" }, { name = "file", completion = "filename" }, "pid" ] +args = { console = "internalConsole", attachCommands = [ "platform select remote-gdb-server", "platform connect {0}", "file {1}", "attach {2}" ] } + [[grammar]] name = "zig" source = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "8d3224c3bd0890fe08358886ebf54fca2ed448a6" } From 6752c7d3474289596c9e062f54beaf07011b6a40 Mon Sep 17 00:00:00 2001 From: Dario Oddenino Date: Fri, 28 Oct 2022 03:31:28 +0200 Subject: [PATCH 130/151] Trim quotes and braces from paths in goto_file_impl (#4370) Co-authored-by: Michael Davis --- helix-term/src/commands.rs | 23 ++++++++---- helix-term/tests/test/commands.rs | 60 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 69870a279f3ce..48bd9e572a631 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1023,6 +1023,7 @@ fn goto_file_vsplit(cx: &mut Context) { goto_file_impl(cx, Action::VerticalSplit); } +/// Goto files in selection. fn goto_file_impl(cx: &mut Context, action: Action) { let (view, doc) = current_ref!(cx.editor); let text = doc.text(); @@ -1032,15 +1033,25 @@ fn goto_file_impl(cx: &mut Context, action: Action) { .map(|r| text.slice(r.from()..r.to()).to_string()) .collect(); let primary = selections.primary(); - if selections.len() == 1 && primary.to() - primary.from() == 1 { - let current_word = movement::move_next_long_word_start( - text.slice(..), - movement::move_prev_long_word_start(text.slice(..), primary, 1), - 1, + // Checks whether there is only one selection with a width of 1 + if selections.len() == 1 && primary.len() == 1 { + let count = cx.count(); + let text_slice = text.slice(..); + // In this case it selects the WORD under the cursor + let current_word = textobject::textobject_word( + text_slice, + primary, + textobject::TextObject::Inside, + count, + true, ); + // Trims some surrounding chars so that the actual file is opened. + let surrounding_chars: &[_] = &['\'', '"', '(', ')']; paths.clear(); paths.push( - text.slice(current_word.from()..current_word.to()) + current_word + .fragment(text_slice) + .trim_matches(surrounding_chars) .to_string(), ); } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index e24ee3e08af69..aadf104bb427d 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -1,6 +1,7 @@ use std::ops::RangeInclusive; use helix_core::diagnostic::Severity; +use helix_term::application::Application; use super::*; @@ -133,3 +134,62 @@ async fn test_selection_duplication() -> anyhow::Result<()> { .await?; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_goto_file_impl() -> anyhow::Result<()> { + let file = tempfile::NamedTempFile::new()?; + + fn match_paths(app: &Application, matches: Vec<&str>) -> usize { + app.editor + .documents() + .filter_map(|d| d.path()?.file_name()) + .filter(|n| matches.iter().any(|m| *m == n.to_string_lossy())) + .count() + } + + // Single selection + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("ione.js%gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + // Multiple selection + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("ione.jstwo.js%gf"), + Some(&|app| { + assert_eq!(2, match_paths(app, vec!["one.js", "two.js"])); + }), + false, + ) + .await?; + + // Cursor on first quote + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one.js'B;gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + // Cursor on last quote + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one.js'bgf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + Ok(()) +} From cefdface3ba6cc2ed60a34fc7115de245c1f58b3 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 28 Oct 2022 07:42:40 -0500 Subject: [PATCH 131/151] Include colons for typable commands in command palette (#4495) Before: Goto next buffer. [buffer-next] After: Goto next buffer. [:buffer-next] --- helix-term/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 48bd9e572a631..fca55b6825584 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2443,8 +2443,8 @@ impl ui::menu::Item for MappableCommand { match self { MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { - Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [{}]", doc, name).into(), + Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(), + None => format!("{} [:{}]", doc, name).into(), }, MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), From 26f21da531179ccc77e70b73e32a1b458c4e225b Mon Sep 17 00:00:00 2001 From: rsteube Date: Fri, 28 Oct 2022 15:22:41 +0200 Subject: [PATCH 132/151] language: added vhs (#4486) --- book/src/generated/lang-support.md | 1 + languages.toml | 13 +++++++++++ runtime/queries/vhs/highlights.scm | 36 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 runtime/queries/vhs/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index caf5d525b16b7..c166af2967edc 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -120,6 +120,7 @@ | v | ✓ | | | `vls` | | vala | ✓ | | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | +| vhs | ✓ | | | | | vue | ✓ | | | `vls` | | wast | ✓ | | | | | wat | ✓ | | | | diff --git a/languages.toml b/languages.toml index 0a87fad243e60..c5c45afc5017b 100644 --- a/languages.toml +++ b/languages.toml @@ -1872,3 +1872,16 @@ formatter = { command = "dfmt" } [[grammar]] name = "d" source = { git = "https://github.com/gdamore/tree-sitter-d", rev="601c4a1e8310fb2f3c43fa8a923d0d27497f3c04" } + +[[language]] +name = "vhs" +scope = "source.vhs" +file-types = ["tape"] +roots = [] +comment-token = "#" +indent = { tab-width = 2, unit = " " } +grammar = "vhs" + +[[grammar]] +name = "vhs" +source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d81f34c011c29ee86dd73b45a8ecc9f2e2bdaf" } diff --git a/runtime/queries/vhs/highlights.scm b/runtime/queries/vhs/highlights.scm new file mode 100644 index 0000000000000..9a2d05cf46356 --- /dev/null +++ b/runtime/queries/vhs/highlights.scm @@ -0,0 +1,36 @@ +[ + "Output" + "Backspace" + "Down" + "Enter" + "Escape" + "Left" + "Right" + "Space" + "Tab" + "Up" + "Set" + "Type" + "Sleep" + "Hide" + "Show" ] @keyword + +[ "FontFamily" + "FontSize" + "Framerate" + "Height" + "LetterSpacing" + "TypingSpeed" + "LineHeight" + "Padding" + "Theme" + "Width" ] @type + +[ "@" ] @operator +(control) @function.macro +(float) @constant.numeric.float +(integer) @constant.numeric.integer +(comment) @comment +(path) @string.special.path +[(string) (json)] @string +(time) @string.special.symbol \ No newline at end of file From c58e1729cef774db6619e394942465c12abd65cd Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sat, 29 Oct 2022 01:20:55 +0200 Subject: [PATCH 133/151] fix: Never create automatic doc popups outside of Insert mode (#4456) --- helix-term/src/commands/lsp.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 3c72cd2a54422..d7617c50ab198 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -9,7 +9,7 @@ use tui::text::{Span, Spans}; use super::{align_view, push_jump, Align, Context, Editor, Open}; use helix_core::{path, Selection}; -use helix_view::{apply_transaction, editor::Action, theme::Style}; +use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style}; use crate::{ compositor::{self, Compositor}, @@ -957,6 +957,14 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { return; } + // If the signature help invocation is automatic, don't show it outside of Insert Mode: + // it very probably means the server was a little slow to respond and the user has + // already moved on to something else, making a signature help popup will just be an + // annoyance, see https://github.com/helix-editor/helix/issues/3112 + if !was_manually_invoked && editor.mode != Mode::Insert { + return; + } + let response = match response { // According to the spec the response should be None if there // are no signatures, but some servers don't follow this. From 5e256e4a98561c2085b1ecae1e39e27bf3744724 Mon Sep 17 00:00:00 2001 From: Matthias Deiml Date: Sat, 29 Oct 2022 17:24:33 +0200 Subject: [PATCH 134/151] Make shell_impl concurrent (#3180) --- helix-term/src/commands.rs | 41 +++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fca55b6825584..172a7b2e7fd0f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -8,7 +8,7 @@ use tui::text::Spans; pub use typed::*; use helix_core::{ - comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, + comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, increment::date_time::DateTimeIncrementor, increment::{number::NumberIncrementor, Increment}, @@ -4630,7 +4630,7 @@ fn shell_keep_pipe(cx: &mut Context) { for (i, range) in selection.ranges().iter().enumerate() { let fragment = range.slice(text); - let (_output, success) = match shell_impl(shell, input, Some(fragment)) { + let (_output, success) = match shell_impl(shell, input, Some(fragment.into())) { Ok(result) => result, Err(err) => { cx.editor.set_error(err.to_string()); @@ -4658,13 +4658,17 @@ fn shell_keep_pipe(cx: &mut Context) { ); } -fn shell_impl( +fn shell_impl(shell: &[String], cmd: &str, input: Option) -> anyhow::Result<(Tendril, bool)> { + tokio::task::block_in_place(|| helix_lsp::block_on(shell_impl_async(shell, cmd, input))) +} + +async fn shell_impl_async( shell: &[String], cmd: &str, - input: Option, + input: Option, ) -> anyhow::Result<(Tendril, bool)> { - use std::io::Write; - use std::process::{Command, Stdio}; + use std::process::Stdio; + use tokio::process::Command; ensure!(!shell.is_empty(), "No shell set"); let mut process = Command::new(&shell[0]); @@ -4687,13 +4691,22 @@ fn shell_impl( return Err(e.into()); } }; - if let Some(input) = input { - let mut stdin = process.stdin.take().unwrap(); - for chunk in input.chunks() { - stdin.write_all(chunk.as_bytes())?; - } - } - let output = process.wait_with_output()?; + let output = if let Some(mut stdin) = process.stdin.take() { + let input_task = tokio::spawn(async move { + if let Some(input) = input { + helix_view::document::to_writer(&mut stdin, encoding::UTF_8, &input).await?; + } + Ok::<_, anyhow::Error>(()) + }); + let (output, _) = tokio::join! { + process.wait_with_output(), + input_task, + }; + output? + } else { + // Process has no stdin, so we just take the output + process.wait_with_output().await? + }; if !output.status.success() { if !output.stderr.is_empty() { @@ -4731,7 +4744,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { for range in selection.ranges() { let fragment = range.slice(text); - let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment)) { + let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { Ok(result) => result, Err(err) => { cx.editor.set_error(err.to_string()); From b5e7501935472f4e68bf42ac9759d54688ccb5e6 Mon Sep 17 00:00:00 2001 From: Jaden Date: Sat, 29 Oct 2022 08:33:23 -0700 Subject: [PATCH 135/151] feat(lang): add kdl grammar (#4481) --- book/src/generated/lang-support.md | 1 + languages.toml | 12 ++++++++++++ runtime/queries/kdl/highlights.scm | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 runtime/queries/kdl/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index c166af2967edc..ee54114a86804 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -60,6 +60,7 @@ | jsonnet | ✓ | | | `jsonnet-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` | | julia | ✓ | | | `julia` | +| kdl | ✓ | | | | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | | lean | ✓ | | | `lean` | diff --git a/languages.toml b/languages.toml index c5c45afc5017b..31a454b0c764e 100644 --- a/languages.toml +++ b/languages.toml @@ -1885,3 +1885,15 @@ grammar = "vhs" [[grammar]] name = "vhs" source = { git = "https://github.com/charmbracelet/tree-sitter-vhs", rev = "c6d81f34c011c29ee86dd73b45a8ecc9f2e2bdaf" } + +[[language]] +name = "kdl" +scope = "source.kdl" +file-types = ["kdl"] +roots = [] +comment-token = "//" +injection-regex = "kdl" + +[[grammar]] +name = "kdl" +source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d15df6610484e1d4b5c987ecad52373" } diff --git a/runtime/queries/kdl/highlights.scm b/runtime/queries/kdl/highlights.scm new file mode 100644 index 0000000000000..d83bde19ceed9 --- /dev/null +++ b/runtime/queries/kdl/highlights.scm @@ -0,0 +1,22 @@ +(comment) @comment +(single_line_comment) @comment + +(node + name: (identifier) @function) +(prop (identifier) @attribute) +(type) @type + +(bare_identifier) @variable.other.member + +(keyword) @keyword + +(string) @string +(number) @constant.numeric +(boolean) @constant.builtin.boolean + +"." @punctuation.delimiter + +"=" @operator + +"{" @punctuation.bracket +"}" @punctuation.bracket From e3eaad14790fb72995ce233a7cd6cd8c249997c0 Mon Sep 17 00:00:00 2001 From: Kristoffer Flottorp <2630397+krfl@users.noreply.github.com> Date: Sat, 29 Oct 2022 17:35:35 +0200 Subject: [PATCH 136/151] Fleetish: Adjustments to resemble official theme and reworked diagnostics to reduce subconjunctival hemorrhage (#4487) --- runtime/themes/fleetish.toml | 53 +++++++++++++++++------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/runtime/themes/fleetish.toml b/runtime/themes/fleetish.toml index a5b09fe4b1896..74340cae8de3e 100644 --- a/runtime/themes/fleetish.toml +++ b/runtime/themes/fleetish.toml @@ -1,11 +1,11 @@ # Author: Kristoffer Flottorp -# Based on a few screenshots of Jetbrains Fleet +# A take on the JetBrains Fleet theme sprinkled with some creative freedom -"type" = { fg = "orange" } # .builtin -"constructor" = { fg = "orange" } -"constant" = { fg = "green" } +"type" = { fg = "yellow" } # .builtin +"constructor" = { fg = "yellow" } +"constant" = { fg = "cyan" } # "constant.builtin" = {} # .boolean -"constant.builtin.boolean" = { fg = "green" } # .boolean +"constant.builtin.boolean" = { fg = "cyan" } # .boolean # "constant.character" = {} #.escape "constant.numeric" = { fg = "yellow" } # .integer / .float "string" = { fg = "pink" } # .regexp @@ -16,31 +16,31 @@ "variable" = { fg = "light" } # .builtin / .parameter # "variable.other" = {} # .member "variable.other.member" = { fg = "purple" } -"label" = { fg = "orange" } +"label" = { fg = "yellow" } # "punctuation" = {} # .delimiter / .bracket -"keyword" = { fg = "green" } # .operator / .directive / .function +"keyword" = { fg = "cyan" } # .operator / .directive / .function # "keyword.control" = { fg = "orange" } # .conditional / .repeat / .import / .return / .exception "operator" = { fg = "light" } "function" = { fg = "blue" } # .builtin / .method / .macro / .special -"function.macro" = { fg = "yellow" } -"function.special" = { fg = "yellow" } -"tag" = { fg = "yellow" } -"special" = { fg = "yellow" } +"function.macro" = { fg = "green" } +"function.special" = { fg = "green" } +"tag" = { fg = "green"} +"special" = { fg = "green" } "namespace" = { fg = "light" } "markup" = { fg = "purple" } # .bold / .italic / .quote "markup.heading" = { fg = "light" } # .marker / .1 / .2 / .3 / .4 / .5 / .6 -"markup.heading.1" = { fg = "orange" } -"markup.heading.2" = { fg = "yellow" } +"markup.heading.1" = { fg = "yellow" } +"markup.heading.2" = { fg = "green" } "markup.heading.3" = { fg = "pink" } "markup.heading.4" = { fg = "purple" } -"markup.heading.5" = { fg = "green" } +"markup.heading.5" = { fg = "cyan" } "markup.heading.6" = { fg = "blue" } -"markup.list" = { fg = "green" } # .unnumbered / .numbered -"markup.link" = { fg = "yellow" } # .url / .label / .text +"markup.list" = { fg = "cyan" } # .unnumbered / .numbered +"markup.link" = { fg = "green" } # .url / .label / .text "markup.raw" = { fg = "pink" } # .inline / .block # "diff" = {} # .plus / .minus -"diff.plus" = { fg = "green" } -"diff.minus" = { fg = "orange" } +"diff.plus" = { fg = "cyan" } +"diff.minus" = { fg = "yellow" } "diff.delta" = { fg = "purple" } # .moved # used in theming @@ -74,11 +74,7 @@ "info" = { fg = "yellow_accent" } "warning" = { fg = "orange_accent" } "error" = { fg = "diff_red_accent" } -"diagnostic" = { fg = "orange", bg = "darkest" } # .hint / .info / .warning / .error -"diagnostic.hint" = { fg = "lightest", bg = "blue_accent" } -"diagnostic.info" = { fg = "lightest", bg = "purple_accent" } -"diagnostic.warning" = { fg = "lightest", bg = "yellow_accent" } -"diagnostic.error" = { fg = "lightest", bg = "orange_accent" } +"diagnostic". underline = { style = "curl" } [palette] darkest = "#0F0F0F" @@ -91,12 +87,13 @@ lightest = "#FFFFFF" dark_gray = "#5B5B5B" light_gray = "#757575" -purple = "#A096F9" -blue = "#52A7F6" -pink = "#E878DE" -green = "#78D0BD" +purple = "#AC9CF9" +blue = "#52A7F6" #"#94C1FA" +pink = "#D898D8" +green = "#AFCB85" +cyan = "#78D0BD" orange = "#ECA775" -yellow = "#F9CA6A" +yellow = "#E5C995" purple_accent = "#6363EE" blue_accent = "#2197F3" From 2935e9da197442620578e07d87cd0607ae4145f1 Mon Sep 17 00:00:00 2001 From: Poliorcetics Date: Sat, 29 Oct 2022 17:36:26 +0200 Subject: [PATCH 137/151] feat: Categorize Rust's keywords using more specific scopes (#4510) --- runtime/queries/rust/highlights.scm | 36 +++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 81f05f7a3d33a..0afb8388efbbc 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -111,23 +111,31 @@ ; ------- (for_expression - "for" @keyword.control) + "for" @keyword.control.repeat) ((identifier) @keyword.control (#match? @keyword.control "^yield$")) + +"in" @keyword.control + +[ + "match" + "if" + "else" +] @keyword.control.conditional + [ "while" "loop" - "in" +] @keyword.control.repeat + +[ "break" "continue" - "match" - "if" - "else" "return" "await" -] @keyword.control +] @keyword.control.return "use" @keyword.control.import (mod_item "mod" @keyword.control.import !body) @@ -143,24 +151,28 @@ "mod" "extern" - "struct" - "enum" "impl" "where" "trait" "for" - "type" - "union" "unsafe" "default" "macro_rules!" - "let" - "async" ] @keyword +[ + "struct" + "enum" + "union" + + "type" +] @keyword.storage.type + +"let" @keyword.storage + "fn" @keyword.function (mutable_specifier) @keyword.storage.modifier.mut From f054a3f3ed445cdfa8c0dc63698659ef30af57b7 Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Sat, 29 Oct 2022 16:41:28 -0400 Subject: [PATCH 138/151] feat(lang): add xml (#4518) --- book/src/generated/lang-support.md | 1 + languages.toml | 20 ++++++++++++++ runtime/queries/xml/highlights.scm | 42 ++++++++++++++++++++++++++++++ runtime/queries/xml/indents.scm | 1 + runtime/queries/xml/injections.scm | 2 ++ 5 files changed, 66 insertions(+) create mode 100644 runtime/queries/xml/highlights.scm create mode 100644 runtime/queries/xml/indents.scm create mode 100644 runtime/queries/xml/injections.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index ee54114a86804..545ec635b4b9f 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -127,5 +127,6 @@ | wat | ✓ | | | | | wgsl | ✓ | | | `wgsl_analyzer` | | xit | ✓ | | | | +| xml | ✓ | | ✓ | | | yaml | ✓ | | ✓ | `yaml-language-server` | | zig | ✓ | ✓ | ✓ | `zls` | diff --git a/languages.toml b/languages.toml index 31a454b0c764e..00e6459dee2df 100644 --- a/languages.toml +++ b/languages.toml @@ -1897,3 +1897,23 @@ injection-regex = "kdl" [[grammar]] name = "kdl" source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d15df6610484e1d4b5c987ecad52373" } + +[[language]] +name = "xml" +scope = "source.xml" +injection-regex = "xml" +file-types = ["xml"] +indent = { tab-width = 2, unit = " " } +roots = [] + +[language.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' +"'" = "'" +"<" = ">" + +[[grammar]] +name = "xml" +source = { git = "https://github.com/RenjiSann/tree-sitter-xml", rev = "422528a43630db6dcc1e222d1c5ee3babd559473" } diff --git a/runtime/queries/xml/highlights.scm b/runtime/queries/xml/highlights.scm new file mode 100644 index 0000000000000..d5940c8a0691f --- /dev/null +++ b/runtime/queries/xml/highlights.scm @@ -0,0 +1,42 @@ +(comment) @comment + +[ + "DOCTYPE" + "ELEMENT" + "ATTLIST" +] @keyword + +[ + "#REQUIRED" + "#IMPLIED" + "#FIXED" + "#PCDATA" +] @keyword.directive + +[ + "EMPTY" + "ANY" + "SYSTEM" + "PUBLIC" +] @constant + +(doctype) @variable +(element_name) @variable + +"xml" @tag +(tag_name) @tag + +[ + "encoding" + "version" + "standalone" +] @attribute +(attribute_name) @attribute + +(system_literal) @string +(pubid_literal) @string +(attribute_value) @string + +[ + "<" ">" "" "" " Date: Sat, 29 Oct 2022 17:23:18 -0400 Subject: [PATCH 139/151] fix: make `scroll` aware of tabs and wide characters (#4519) --- helix-term/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 172a7b2e7fd0f..adb4802d8f456 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1360,7 +1360,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { let range = doc.selection(view.id).primary(); let text = doc.text().slice(..); - let cursor = coords_at_pos(text, range.cursor(text)); + let cursor = visual_coords_at_pos(text, range.cursor(text), doc.tab_width()); let doc_last_line = doc.text().len_lines().saturating_sub(1); let last_line = view.last_line(doc); @@ -1392,7 +1392,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { // If cursor needs moving, replace primary selection if line != cursor.row { - let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end + let head = pos_at_visual_coords(text, Position::new(line, cursor.col), doc.tab_width()); // this func will properly truncate to line end let anchor = if cx.editor.mode == Mode::Select { range.anchor From 908529ccac153c12fd3b8e051a8f8ef68120d94d Mon Sep 17 00:00:00 2001 From: Triton171 Date: Sun, 30 Oct 2022 17:45:58 +0100 Subject: [PATCH 140/151] Update LaTex grammar (#4528) Fix comment injection & add highlighting for math delimiters. --- languages.toml | 2 +- runtime/queries/latex/highlights.scm | 6 ++++++ runtime/queries/latex/injections.scm | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/languages.toml b/languages.toml index 00e6459dee2df..73915fc294846 100644 --- a/languages.toml +++ b/languages.toml @@ -570,7 +570,7 @@ indent = { tab-width = 4, unit = "\t" } [[grammar]] name = "latex" -source = { git = "https://github.com/latex-lsp/tree-sitter-latex", rev = "b3b2cf27f33e71438ebe46934900b1153901c6f2" } +source = { git = "https://github.com/latex-lsp/tree-sitter-latex", rev = "8c75e93cd08ccb7ce1ccab22c1fbd6360e3bcea6" } [[language]] name = "lean" diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm index e39226a276c1f..3174d80b939ca 100644 --- a/runtime/queries/latex/highlights.scm +++ b/runtime/queries/latex/highlights.scm @@ -29,6 +29,12 @@ (#eq? @punctuation.delimiter "&")) ["[" "]" "{" "}"] @punctuation.bracket ; "(" ")" has no syntactical meaning in LaTeX +(math_delimiter + left_command: _ @punctuation.delimiter + left_delimiter: _ @punctuation.delimiter + right_command: _ @punctuation.delimiter + right_delimiter: _ @punctuation.delimiter +) ;; General environments (begin diff --git a/runtime/queries/latex/injections.scm b/runtime/queries/latex/injections.scm index 321c90add3710..d3fdb0ca71781 100644 --- a/runtime/queries/latex/injections.scm +++ b/runtime/queries/latex/injections.scm @@ -1,2 +1,2 @@ -((comment) @injection.content +((line_comment) @injection.content (#set! injection.language "comment")) From f6710879d1a1e587d17800682ba0e1045f35e7a9 Mon Sep 17 00:00:00 2001 From: seshotake <116971836+seshotake@users.noreply.github.com> Date: Sun, 30 Oct 2022 16:54:37 +0000 Subject: [PATCH 141/151] Update SQL grammar (#4529) --- languages.toml | 2 +- runtime/queries/sql/highlights.scm | 52 ++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/languages.toml b/languages.toml index 73915fc294846..9c0293f0bb555 100644 --- a/languages.toml +++ b/languages.toml @@ -1371,7 +1371,7 @@ injection-regex = "sql" [[grammar]] name = "sql" -source = { git = "https://github.com/DerekStride/tree-sitter-sql", rev = "0caa7fa2ee00e0b770493a79d4efacc1fc376cc5" } +source = { git = "https://github.com/DerekStride/tree-sitter-sql", rev = "2743c7b5e710e6854d4e8c14c302548b436e2a1f" } [[language]] name = "gdscript" diff --git a/runtime/queries/sql/highlights.scm b/runtime/queries/sql/highlights.scm index 5025352e7ecc5..ece8be333c780 100644 --- a/runtime/queries/sql/highlights.scm +++ b/runtime/queries/sql/highlights.scm @@ -1,3 +1,34 @@ +(keyword_gist) @function.builtin +(keyword_btree) @function.builtin +(keyword_btree) @function.builtin +(keyword_hash) @function.builtin +(keyword_spgist) @function.builtin +(keyword_gin) @function.builtin +(keyword_brin) @function.builtin +(keyword_float) @function.builtin + +(invocation + name: (identifier) @function.builtin + parameter: [(field)]? @variable.other.member) + +(count + name: (identifier) @function.builtin + parameter: [(field)]? @variable.other.member) + +(table_reference + name: (identifier) @namespace) + +(relation + table_alias: (identifier) @variable.parameter) + +(field + name: (identifier) @variable.other.member) + +(field + table_alias: (identifier) @variable.parameter + name: (identifier) @variable.other.member) + + (comment) @comment [ @@ -5,6 +36,12 @@ ")" ] @punctuation.bracket +[ + ";" + "," + "." +] @punctuation.delimiter + [ "*" "+" @@ -29,11 +66,8 @@ (literal) @string -(set_schema schema: (identifier) @namespace) -(table_reference schema: (identifier) @namespace) -(table_expression schema: (identifier) @namespace) -(all_fields schema: (identifier) @namespace) -(field schema: (identifier) @namespace) +((literal) @constant.numeric + (#match? @constant.numeric "^(-?\d*\.?\d*)$")) [ (keyword_select) @@ -54,8 +88,10 @@ (keyword_lateral) (keyword_on) (keyword_not) - (keyword_order_by) - (keyword_group_by) + (keyword_order) + (keyword_group) + (keyword_partition) + (keyword_by) (keyword_having) (keyword_desc) (keyword_asc) @@ -89,6 +125,8 @@ (keyword_auto_increment) (keyword_default) (keyword_cascade) + (keyword_between) + (keyword_window) (keyword_with) (keyword_no) (keyword_data) From f41f28b6627bd2ae0152f468cde80df167889a2d Mon Sep 17 00:00:00 2001 From: Konstantin Podsvirov Date: Sun, 30 Oct 2022 19:55:31 +0300 Subject: [PATCH 142/151] Update windows install instructions (#4530) --- book/src/install.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/book/src/install.md b/book/src/install.md index 6e2a1f3dfafc5..a041d6511926a 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -52,7 +52,8 @@ sudo xbps-install helix ## Windows -Helix can be installed using [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/). +Helix can be installed using [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) +or [MSYS2](https://msys2.org/). **Scoop:** @@ -66,6 +67,23 @@ scoop install helix choco install helix ``` +**MSYS2:** + +``` +pacman -S mingw-w64-i686-helix +``` + +or + +``` +pacman -S mingw-w64-x86_64-helix +``` + +or + +``` +pacman -S mingw-w64-ucrt-x86_64-helix +``` ## Build from source From df3c6412acc2c0948450653a67a4993092abf5fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 18:21:51 -0500 Subject: [PATCH 143/151] build(deps): bump cachix/cachix-action from 11 to 12 (#4547) Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 11 to 12. - [Release notes](https://github.com/cachix/cachix-action/releases) - [Commits](https://github.com/cachix/cachix-action/compare/v11...v12) --- updated-dependencies: - dependency-name: cachix/cachix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cachix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index bc72bb78df21d..20035678707ae 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -17,7 +17,7 @@ jobs: uses: cachix/install-nix-action@v18 - name: Authenticate with Cachix - uses: cachix/cachix-action@v11 + uses: cachix/cachix-action@v12 with: name: helix authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} From 9df43584924a72be88f537ac432e5976430fa3f1 Mon Sep 17 00:00:00 2001 From: hh9527 Date: Tue, 1 Nov 2022 07:48:01 +0800 Subject: [PATCH 144/151] Support WIT grammar (#4525) --- book/src/generated/lang-support.md | 1 + languages.toml | 22 ++++++++++ runtime/queries/wit/highlights.scm | 67 ++++++++++++++++++++++++++++++ runtime/queries/wit/indents.scm | 13 ++++++ 4 files changed, 103 insertions(+) create mode 100644 runtime/queries/wit/highlights.scm create mode 100644 runtime/queries/wit/indents.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 545ec635b4b9f..38d121b5ac64e 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -126,6 +126,7 @@ | wast | ✓ | | | | | wat | ✓ | | | | | wgsl | ✓ | | | `wgsl_analyzer` | +| wit | ✓ | | ✓ | | | xit | ✓ | | | | | xml | ✓ | | ✓ | | | yaml | ✓ | | ✓ | `yaml-language-server` | diff --git a/languages.toml b/languages.toml index 9c0293f0bb555..ee31f3728c4da 100644 --- a/languages.toml +++ b/languages.toml @@ -1917,3 +1917,25 @@ roots = [] [[grammar]] name = "xml" source = { git = "https://github.com/RenjiSann/tree-sitter-xml", rev = "422528a43630db6dcc1e222d1c5ee3babd559473" } + +[[language]] +name = "wit" +scope = "source.wit" +injection-regex = "wit" +file-types = ["wit"] +roots = [] +comment-token = "//" +indent = { tab-width = 2, unit = " " } + +[language.auto-pairs] +'(' = ')' +'{' = '}' +'[' = ']' +'"' = '"' +"'" = "'" +"<" = ">" + +[[grammar]] +name = "wit" +source = { git = "https://github.com/hh9527/tree-sitter-wit", rev = "c917790ab9aec50c5fd664cbfad8dd45110cfff3" } + diff --git a/runtime/queries/wit/highlights.scm b/runtime/queries/wit/highlights.scm new file mode 100644 index 0000000000000..45754a5a8df2d --- /dev/null +++ b/runtime/queries/wit/highlights.scm @@ -0,0 +1,67 @@ +(line_comment) @comment.line +(block_comment) @comment.block +(ty (ident) @type) + +(item_type name: (ident) @type) +(item_record name: (ident) @type) +(item_variant name: (ident) @type) +(item_flags name: (ident) @type) +(item_enum name: (ident) @type) +(item_union name: (ident) @type) +(item_resource name: (ident) @type) + +(item_use from: (ident) @namespace) +(use_item name: (ident) @type) +(item_func name: (ident) @function) +(method name: (ident) @function.method) +(fields (named_ty name: (ident) @variable.other.member)) +(input (args (named_ty name: (ident) @variable.parameter))) +(output (args (named_ty name: (ident) @variable.other.member))) +(flags (ident) @constant) +(enum_items (ident) @constant) +(variant_item tag: (ident) @type.enum.variant) + +[ + (unit) + + "u8" "u16" "u32" "u64" + "s8" "s16" "s32" "s64" + "float32" "float64" + "char" "bool" "string" +] @type.builtin + +[ + "list" + "option" + "result" + "tuple" + "future" + "stream" +] @function.macro + +[ "," ":" ] @punctuation.delimiter +[ "(" ")" "{" "}" "<" ">" ] @punctuation.bracket +[ "=" "->" ] @operator + +[ + "record" + "flags" + "variant" + "enum" + "union" + "type" + "resource" +] @keyword.storage.type + +"func" @keyword + +[ + "static" +] @keyword.storage.modifier + +[ + (star) + "use" + "as" + "from" +] @keyword.control.import diff --git a/runtime/queries/wit/indents.scm b/runtime/queries/wit/indents.scm new file mode 100644 index 0000000000000..db6c148bf52f4 --- /dev/null +++ b/runtime/queries/wit/indents.scm @@ -0,0 +1,13 @@ +[ + (use_items) + (fields) + (variant_items) + (variant_payload) + (flags) + (enum_items) + (union_items) + (args) + (resource_items) +] @indent + +[ "}" ")" ] @outdent From ed7ea8c9ba639daebdbc81630bd789aeb344e2d4 Mon Sep 17 00:00:00 2001 From: seshotake <116971836+seshotake@users.noreply.github.com> Date: Tue, 1 Nov 2022 02:23:09 +0200 Subject: [PATCH 145/151] add highlights for env and ini file formats (#4536) --- book/src/generated/lang-support.md | 2 ++ languages.toml | 27 ++++++++++++++++++++++++++- runtime/queries/env/highlights.scm | 19 +++++++++++++++++++ runtime/queries/ini/highlights.scm | 6 ++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 runtime/queries/env/highlights.scm create mode 100644 runtime/queries/ini/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 38d121b5ac64e..411e67b8a974e 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -27,6 +27,7 @@ | elixir | ✓ | ✓ | | `elixir-ls` | | elm | ✓ | | | `elm-language-server` | | elvish | ✓ | | | `elvish` | +| env | ✓ | | | | | erb | ✓ | | | | | erlang | ✓ | ✓ | | `erlang_ls` | | esdl | ✓ | | | | @@ -53,6 +54,7 @@ | html | ✓ | | | `vscode-html-language-server` | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | +| ini | ✓ | | | | | java | ✓ | | | `jdtls` | | javascript | ✓ | ✓ | ✓ | `typescript-language-server` | | jsdoc | ✓ | | | | diff --git a/languages.toml b/languages.toml index ee31f3728c4da..95c37945a8c64 100644 --- a/languages.toml +++ b/languages.toml @@ -1938,4 +1938,29 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "wit" source = { git = "https://github.com/hh9527/tree-sitter-wit", rev = "c917790ab9aec50c5fd664cbfad8dd45110cfff3" } - + +[[language]] +name = "env" +scope = "source.env" +file-types = [".env", ".env.local", ".env.development", ".env.production"] +injection-regex = "env" +comment-token = "#" +indent = { tab-width = 4, unit = "\t" } +roots = [] + +[[grammar]] +name = "env" +source = { git = "https://github.com/seshotake/tree-sitter-env", rev = "e6c6bb1e7b51d481cba463fe949f083cf22d81f7" } + +[[language]] +name = "ini" +scope = "source.ini" +file-types = ["ini"] +injection-regex = "ini" +comment-token = "#" +indent = { tab-width = 4, unit = "\t" } +roots = [] + +[[grammar]] +name = "ini" +source = { git = "https://github.com/justinmk/tree-sitter-ini", rev = "4d247fb876b4ae6b347687de4a179511bf67fcbc" } diff --git a/runtime/queries/env/highlights.scm b/runtime/queries/env/highlights.scm new file mode 100644 index 0000000000000..6a27e8e5a0494 --- /dev/null +++ b/runtime/queries/env/highlights.scm @@ -0,0 +1,19 @@ +(env_variable (quoted_string)) @string +(env_variable (unquoted_string)) @string + +(env_key) @keyword + +((variable) @keyword + (#match? @keyword "^([A-Z][A-Z_0-9]*)$")) + +[ + "{" + "}" +] @punctuation.bracket + +[ + "$" + "=" +] @operator + +(comment) @comment \ No newline at end of file diff --git a/runtime/queries/ini/highlights.scm b/runtime/queries/ini/highlights.scm new file mode 100644 index 0000000000000..6277a0676b0d8 --- /dev/null +++ b/runtime/queries/ini/highlights.scm @@ -0,0 +1,6 @@ +(section_name) @namespace + +(setting_name) @keyword +(setting_value) @string + +(comment) @comment From 9b247b1104ab34d55ca383daafab2a26dae31253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Ho=C3=9F?= Date: Tue, 1 Nov 2022 01:27:53 +0100 Subject: [PATCH 146/151] Update SSH client config grammar & highlight queries (#4538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastian Hoß --- languages.toml | 2 +- .../queries/sshclientconfig/highlights.scm | 170 +++++++++--------- 2 files changed, 86 insertions(+), 86 deletions(-) diff --git a/languages.toml b/languages.toml index 95c37945a8c64..adce81c5948ea 100644 --- a/languages.toml +++ b/languages.toml @@ -1518,7 +1518,7 @@ roots = [] [[grammar]] name = "sshclientconfig" -source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev = "769d7a01a2e5493b4bb5a51096c6bf4be130b024" } +source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev = "e45c6d5c71657344d4ecaf87dafae7736f776c57" } [[language]] name = "scheme" diff --git a/runtime/queries/sshclientconfig/highlights.scm b/runtime/queries/sshclientconfig/highlights.scm index 83a212a20a42d..b54da58758ee4 100644 --- a/runtime/queries/sshclientconfig/highlights.scm +++ b/runtime/queries/sshclientconfig/highlights.scm @@ -1,17 +1,17 @@ -(host) @keyword -(host_value) @identifier +(host) @namespace +(host_value) @string -(match) @keyword -(match_value) @identifier +(match) @namespace +(match_value) @string (add_keys_to_agent) @keyword -(add_keys_to_agent_value) @boolean +(add_keys_to_agent_value) @constant.builtin.boolean (address_family) @keyword -(address_family_value) @type +(address_family_value) @constant.builtin (batch_mode) @keyword -(batch_mode_value) @boolean +(batch_mode_value) @constant.builtin.boolean (bind_address) @keyword (bind_address_value) @string @@ -20,165 +20,165 @@ (bind_interface_value) @string (canonical_domains) @keyword -(canonical_domains_value) @identifier +(canonical_domains_value) @string (canonicalize_fallback_local) @keyword -(canonicalize_fallback_local_value) @boolean +(canonicalize_fallback_local_value) @constant.builtin.boolean (canonicalize_hostname) @keyword -(canonicalize_hostname_value) @boolean +(canonicalize_hostname_value) @constant.builtin (canonicalize_max_dots) @keyword -(canonicalize_max_dots_value) @number +(canonicalize_max_dots_value) @constant.numeric.integer (canonicalize_permitted_cnames) @keyword -(canonicalize_permitted_cnames_value) @identifier +(canonicalize_permitted_cnames_value) @string (ca_signature_algorithms) @keyword -(ca_signature_algorithms_value) @identifier +(ca_signature_algorithms_value) @string (certificate_file) @keyword -(certificate_file_value) @file +(certificate_file_value) @string.special.path (challenge_response_authentication) @keyword -(challenge_response_authentication_value) @boolean +(challenge_response_authentication_value) @constant.builtin.boolean (check_host_ip) @keyword -(check_host_ip_value) @boolean +(check_host_ip_value) @constant.builtin.boolean (cipher) @keyword -(cipher_value) @identifier +(cipher_value) @string (ciphers) @keyword -(ciphers_value) @identifier +(ciphers_value) @string (clear_all_forwardings) @keyword -(clear_all_forwardings_value) @boolean +(clear_all_forwardings_value) @constant.builtin.boolean (comment) @comment (compression) @keyword -(compression_value) @boolean +(compression_value) @constant.builtin.boolean (connect_timeout) @keyword -(connect_timeout_value) @number +(connect_timeout_value) @constant.numeric.integer (connection_attempts) @keyword -(connection_attempts_value) @number +(connection_attempts_value) @constant.numeric.integer (control_master) @keyword -(control_master_value) @type +(control_master_value) @constant.builtin (control_path) @keyword -(control_path_value) @file +(control_path_value) @string.special.path (control_persist) @keyword -(control_persist_value) @type +(control_persist_value) @constant.builtin (dynamic_forward) @keyword (dynamic_forward_value) @string (enable_ssh_keysign) @keyword -(enable_ssh_keysign_value) @boolean +(enable_ssh_keysign_value) @constant.builtin.boolean (escape_char) @keyword -(escape_char_value) @string +(escape_char_value) @constant.character.escape (exit_on_forward_failure) @keyword -(exit_on_forward_failure_value) @boolean +(exit_on_forward_failure_value) @constant.builtin.boolean (fingerprint_hash) @keyword -(fingerprint_hash_value) @identifier +(fingerprint_hash_value) @constant.builtin (fork_after_authentication) @keyword -(fork_after_authentication_value) @boolean +(fork_after_authentication_value) @constant.builtin.boolean (forward_agent) @keyword -(forward_agent_value) @boolean +(forward_agent_value) @string (forward_x11) @keyword -(forward_x11_value) @boolean +(forward_x11_value) @constant.builtin.boolean (forward_x11_timeout) @keyword -(forward_x11_timeout_value) @time +(forward_x11_timeout_value) @constant.numeric.integer (forward_x11_trusted) @keyword -(forward_x11_trusted_value) @boolean +(forward_x11_trusted_value) @constant.builtin.boolean (gateway_ports) @keyword -(gateway_ports_value) @boolean +(gateway_ports_value) @constant.builtin.boolean (global_known_hosts_file) @keyword -(global_known_hosts_file_value) @file +(global_known_hosts_file_value) @string.special.path (gssapi_authentication) @keyword -(gssapi_authentication_value) @boolean +(gssapi_authentication_value) @constant.builtin.boolean (gssapi_client_identity) @keyword (gssapi_client_identity_value) @string (gssapi_delegate_credentials) @keyword -(gssapi_delegate_credentials_value) @boolean +(gssapi_delegate_credentials_value) @constant.builtin.boolean (gssapi_kex_algorithms) @keyword -(gssapi_kex_algorithms_value) @identifier +(gssapi_kex_algorithms_value) @string (gssapi_key_exchange) @keyword -(gssapi_key_exchange_value) @boolean +(gssapi_key_exchange_value) @constant.builtin.boolean (gssapi_renewal_forces_rekey) @keyword -(gssapi_renewal_forces_rekey_value) @boolean +(gssapi_renewal_forces_rekey_value) @constant.builtin.boolean (gssapi_server_identity) @keyword (gssapi_server_identity_value) @string (gssapi_trust_dns) @keyword -(gssapi_trust_dns_value) @boolean +(gssapi_trust_dns_value) @constant.builtin.boolean (hash_known_hosts) @keyword -(hash_known_hosts_value) @boolean +(hash_known_hosts_value) @constant.builtin.boolean (host_key_algorithms) @keyword -(host_key_algorithms_value) @identifier +(host_key_algorithms_value) @string (host_key_alias) @keyword (host_key_alias_value) @string (hostbased_accepted_algorithms) @keyword -(hostbased_accepted_algorithms_value) @identifier +(hostbased_accepted_algorithms_value) @string (hostbased_authentication) @keyword -(hostbased_authentication_value) @boolean +(hostbased_authentication_value) @constant.builtin.boolean (hostname) @keyword (hostname_value) @string (identities_only) @keyword -(identities_only_value) @boolean +(identities_only_value) @constant.builtin.boolean (identity_agent) @keyword (identity_agent_value) @string (identity_file) @keyword -(identity_file_value) @file +(identity_file_value) @string.special.path (ignore_unknown) @keyword (ignore_unknown_value) @string -(include) @keyword -(include_value) @file +(include) @function.macro +(include_value) @string.special.path (ip_qos) @keyword -(ip_qos_value) @type +(ip_qos_value) @constant.builtin (kbd_interactive_authentication) @keyword -(kbd_interactive_authentication_value) @boolean +(kbd_interactive_authentication_value) @constant.builtin.boolean (kbd_interactive_devices) @keyword -(kbd_interactive_devices_value) @type +(kbd_interactive_devices_value) @string (kex_algorithms) @keyword -(kex_algorithms_value) @identifier +(kex_algorithms_value) @string (known_hosts_command) @keyword (known_hosts_command_value) @string @@ -190,25 +190,25 @@ (local_forward_value) @string (log_level) @keyword -(log_level_value) @type +(log_level_value) @constant.builtin (log_verbose) @keyword (log_verbose_value) @string (macs) @keyword -(macs_value) @identifier +(macs_value) @string (no_host_authentication_for_localhost) @keyword -(no_host_authentication_for_localhost_value) @boolean +(no_host_authentication_for_localhost_value) @constant.builtin.boolean (number_of_password_prompts) @keyword -(number_of_password_prompts_value) @number +(number_of_password_prompts_value) @constant.numeric.integer (password_authentication) @keyword -(password_authentication_value) @boolean +(password_authentication_value) @constant.builtin.boolean (permit_local_command) @keyword -(permit_local_command_value) @boolean +(permit_local_command_value) @constant.builtin.boolean (permit_remote_open) @keyword (permit_remote_open_value) @string @@ -217,13 +217,13 @@ (pkcs11_provider_value) @string (port) @keyword -(port_value) @number +(port_value) @constant.numeric.integer (preferred_authentications) @keyword -(preferred_authentications_value) @type +(preferred_authentications_value) @string (protocol) @keyword -(protocol_value) @number +(protocol_value) @constant.numeric.integer (proxy_command) @keyword (proxy_command_value) @string @@ -232,16 +232,16 @@ (proxy_jump_value) @string (proxy_use_fdpass) @keyword -(proxy_use_fdpass_value) @boolean +(proxy_use_fdpass_value) @constant.builtin.boolean (pubkey_accepted_algorithms) @keyword -(pubkey_accepted_algorithms_value) @identifier +(pubkey_accepted_algorithms_value) @string (pubkey_accepted_key_types) @keyword -(pubkey_accepted_key_types_value) @identifier +(pubkey_accepted_key_types_value) @string (pubkey_authentication) @keyword -(pubkey_authentication_value) @boolean +(pubkey_authentication_value) @constant.builtin (rekey_limit) @keyword (rekey_limit_value) @string @@ -253,10 +253,10 @@ (remote_forward_value) @string (request_tty) @keyword -(request_tty_value) @type +(request_tty_value) @constant.builtin (revoked_host_keys) @keyword -(revoked_host_keys_value) @file +(revoked_host_keys_value) @string.special.path (security_key_provider) @keyword (security_key_provider_value) @string @@ -265,60 +265,60 @@ (send_env_value) @string (server_alive_count_max) @keyword -(server_alive_count_max_value) @number +(server_alive_count_max_value) @constant.numeric.integer (server_alive_interval) @keyword -(server_alive_interval_value) @number +(server_alive_interval_value) @constant.numeric.integer (session_type) @keyword -(session_type_value) @type +(session_type_value) @constant.builtin (set_env) @keyword (set_env_value) @string (stdin_null) @keyword -(stdin_null_value) @boolean +(stdin_null_value) @constant.builtin.boolean (stream_local_bind_mask) @keyword (stream_local_bind_mask_value) @string (stream_local_bind_unlink) @keyword -(stream_local_bind_unlink_value) @boolean +(stream_local_bind_unlink_value) @constant.builtin.boolean (strict_host_key_checking) @keyword -(strict_host_key_checking_value) @type +(strict_host_key_checking_value) @constant.builtin (syslog_facility) @keyword -(syslog_facility_value) @type +(syslog_facility_value) @constant.builtin (tcp_keep_alive) @keyword -(tcp_keep_alive_value) @boolean +(tcp_keep_alive_value) @constant.builtin.boolean (keep_alive) @keyword -(keep_alive_value) @boolean +(keep_alive_value) @constant.builtin.boolean (tunnel) @keyword -(tunnel_value) @type +(tunnel_value) @constant.builtin (tunnel_device) @keyword (tunnel_device_value) @string (update_host_keys) @keyword -(update_host_keys_value) @type +(update_host_keys_value) @constant.builtin (use_keychain) @keyword -(use_keychain_value) @boolean +(use_keychain_value) @constant.builtin.boolean (user) @keyword (user_value) @string (user_known_hosts_file) @keyword -(user_known_hosts_file_value) @file +(user_known_hosts_file_value) @string.special.path (verify_host_key_dns) @keyword -(verify_host_key_dns_value) @type +(verify_host_key_dns_value) @constant.builtin (visual_host_key) @keyword -(visual_host_key_value) @boolean +(visual_host_key_value) @constant.builtin.boolean (xauth_location) @keyword -(xauth_location_value) @file +(xauth_location_value) @string.special.path From 92b13f9e71c7d8adae4a6c1fa5f0135137c3c290 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 19:29:01 -0500 Subject: [PATCH 147/151] build(deps): bump cc from 1.0.73 to 1.0.74 (#4549) Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.0.73 to 1.0.74. - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.73...1.0.74) --- updated-dependencies: - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 163b3ad48bb79..e4d4568ad4b44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,9 +92,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cfg-if" From 3792d9ebb7ffc3294987d3fae0c14511cbdbb615 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 19:29:32 -0500 Subject: [PATCH 148/151] build(deps): bump lsp-types from 0.93.1 to 0.93.2 (#4550) Bumps [lsp-types](https://github.com/gluon-lang/lsp-types) from 0.93.1 to 0.93.2. - [Release notes](https://github.com/gluon-lang/lsp-types/releases) - [Changelog](https://github.com/gluon-lang/lsp-types/blob/master/CHANGELOG.md) - [Commits](https://github.com/gluon-lang/lsp-types/compare/v0.93.1...v0.93.2) --- updated-dependencies: - dependency-name: lsp-types dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 e4d4568ad4b44..ecbc0a14d7d6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,9 +684,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.93.1" +version = "0.93.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bcfee315dde785ba887edb540b08765fd7df75a7d948844be6bf5712246734" +checksum = "9be6e9c7e2d18f651974370d7aff703f9513e0df6e464fd795660edc77e6ca51" dependencies = [ "bitflags", "serde", From 79c7203a388e1ac7aa6aebfc2e4c9a91b6efc97a Mon Sep 17 00:00:00 2001 From: Jonas Everaert <62475953+Jomy10@users.noreply.github.com> Date: Tue, 1 Nov 2022 01:30:08 +0100 Subject: [PATCH 149/151] Added missing keywords to wat (wasm) hightlights (#4542) added "if", "then", "else", "block", "loop", "end" and "mut" to the wat highlights. --- runtime/queries/wat/highlights.scm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/queries/wat/highlights.scm b/runtime/queries/wat/highlights.scm index 007e3bbff8e03..93f03aac09c5d 100644 --- a/runtime/queries/wat/highlights.scm +++ b/runtime/queries/wat/highlights.scm @@ -1,4 +1,7 @@ -["module" "func" "param" "result" "type" "memory" "elem" "data" "table" "global"] @keyword +[ + "module" "func" "param" "result" "type" "memory" "elem" "data" "table" "global" + "if" "then" "else" "block" "loop" "end" "mut" +] @keyword ["import" "export"] @keyword.control.import From e5319ea8c52d6c97ce901426c2714a26f535be0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 19:31:08 -0500 Subject: [PATCH 150/151] build(deps): bump once_cell from 1.15.0 to 1.16.0 (#4548) Bumps [once_cell](https://github.com/matklad/once_cell) from 1.15.0 to 1.16.0. - [Release notes](https://github.com/matklad/once_cell/releases) - [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md) - [Commits](https://github.com/matklad/once_cell/compare/v1.15.0...v1.16.0) --- updated-dependencies: - dependency-name: once_cell dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 +- helix-loader/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecbc0a14d7d6e..93459aa07fa64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,9 +753,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "parking_lot" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 7585c347607cd..45272f9800582 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -26,7 +26,7 @@ unicode-general-category = "0.6" # slab = "0.4.2" slotmap = "1.0" tree-sitter = "0.20" -once_cell = "1.15" +once_cell = "1.16" arc-swap = "1" regex = "1" bitflags = "1.3" diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index b4541de5f147a..760205e1f208c 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -19,7 +19,7 @@ serde = { version = "1.0", features = ["derive"] } toml = "0.5" etcetera = "0.4" tree-sitter = "0.20" -once_cell = "1.15" +once_cell = "1.16" log = "0.4" # TODO: these two should be on !wasm32 only diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 30de5589fb9c5..485cabe90819a 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -32,7 +32,7 @@ helix-dap = { version = "0.6", path = "../helix-dap" } helix-loader = { version = "0.6", path = "../helix-loader" } anyhow = "1" -once_cell = "1.15" +once_cell = "1.16" which = "4.2" diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index b96a537d089f5..a2a88001bffd2 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -23,7 +23,7 @@ helix-dap = { version = "0.6", path = "../helix-dap" } crossterm = { version = "0.25", optional = true } # Conversion traits -once_cell = "1.15" +once_cell = "1.16" url = "2" arc-swap = { version = "1.5.1" } From 3881fef39d01c94a09b8f5da67decc2c3ccb3660 Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Tue, 1 Nov 2022 03:52:03 +0300 Subject: [PATCH 151/151] build(nix): update nci, fixup flake (#4537) --- flake.lock | 71 ++++++++++++----- flake.nix | 225 ++++++++++++++++++++++++++--------------------------- 2 files changed, 165 insertions(+), 131 deletions(-) diff --git a/flake.lock b/flake.lock index f28ec884d2557..cfa227e31784d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,22 @@ { "nodes": { + "all-cabal-json": { + "flake": false, + "locked": { + "lastModified": 1665552503, + "narHash": "sha256-r14RmRSwzv5c+bWKUDaze6pXM7nOsiz1H8nvFHJvufc=", + "owner": "nix-community", + "repo": "all-cabal-json", + "rev": "d7c0434eebffb305071404edcf9d5cd99703878e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "hackage", + "repo": "all-cabal-json", + "type": "github" + } + }, "crane": { "flake": false, "locked": { @@ -19,11 +36,11 @@ "devshell": { "flake": false, "locked": { - "lastModified": 1660811669, - "narHash": "sha256-V6lmsaLNFz41myppL0yxglta92ijkSvpZ+XVygAh+bU=", + "lastModified": 1666548262, + "narHash": "sha256-4DyN4KXqQQsCw0vCXkMThw4b5Q4/q87ZZgRb4st8COc=", "owner": "numtide", "repo": "devshell", - "rev": "c2feacb46ee69949124c835419861143c4016fb5", + "rev": "c8ce8ed81726079c398f5f29c4b68a7d6a3c2fa2", "type": "github" }, "original": { @@ -38,6 +55,7 @@ "nci", "nixpkgs" ], + "all-cabal-json": "all-cabal-json", "crane": "crane", "devshell": [ "nci", @@ -47,6 +65,7 @@ "nci", "nixpkgs" ], + "ghc-utils": "ghc-utils", "gomod2nix": [ "nci", "nixpkgs" @@ -69,11 +88,11 @@ ] }, "locked": { - "lastModified": 1662176993, - "narHash": "sha256-Sy7DsGAveDUFBb6YDsUSYZd/AcXfP/MOMIwMt/NgY84=", + "lastModified": 1666993587, + "narHash": "sha256-4cLrs+CwWnceYXnCpL5gO3bybS9CjLxUoTEKjB2QFtg=", "owner": "nix-community", "repo": "dream2nix", - "rev": "809bc5940214744eb29778a9a0b03f161979c1b2", + "rev": "2b7456e3d2f0053bc2474fb0c461dd468545277f", "type": "github" }, "original": { @@ -84,11 +103,11 @@ }, "flake-utils": { "locked": { - "lastModified": 1656928814, - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "type": "github" }, "original": { @@ -97,6 +116,22 @@ "type": "github" } }, + "ghc-utils": { + "flake": false, + "locked": { + "lastModified": 1662774800, + "narHash": "sha256-1Rd2eohGUw/s1tfvkepeYpg8kCEXiIot0RijapUjAkE=", + "ref": "refs/heads/master", + "rev": "bb3a2d3dc52ff0253fb9c2812bd7aa2da03e0fea", + "revCount": 1072, + "type": "git", + "url": "https://gitlab.haskell.org/bgamari/ghc-utils" + }, + "original": { + "type": "git", + "url": "https://gitlab.haskell.org/bgamari/ghc-utils" + } + }, "nci": { "inputs": { "devshell": "devshell", @@ -109,11 +144,11 @@ ] }, "locked": { - "lastModified": 1662177071, - "narHash": "sha256-x6XF//RdZlw81tFAYM1TkjY+iQIpyMCWZ46r9o4wVQY=", + "lastModified": 1667232647, + "narHash": "sha256-cFo7G8BqYShgL9m7yD6p+SHAZ+aIt2guuF69LV235n8=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "65270dea87bb82fc02102a15221677eea237680e", + "rev": "16082f7b4e42ce140a562fa630bcf8e96eadeb59", "type": "github" }, "original": { @@ -124,11 +159,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1662019588, - "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", + "lastModified": 1667142599, + "narHash": "sha256-OLJxsg9VqfKjFkerOxWtNIkibsCvxsv5A8wNWO1MeWk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2da64a81275b68fdad38af669afeda43d401e94b", + "rev": "412b9917cea092f3d39f9cd5dead4effd5bc4053", "type": "github" }, "original": { @@ -153,11 +188,11 @@ ] }, "locked": { - "lastModified": 1662087605, - "narHash": "sha256-Gpf2gp2JenKGf+TylX/YJpttY2bzsnvAMLdLaxoZRyU=", + "lastModified": 1667184938, + "narHash": "sha256-/kuCiXuAxiD0c0zrfDvJ1Yba3FuVdRk/ROfb393AeX4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "60c2cfaa8b90ed8cebd18b214fac8682dcf222dd", + "rev": "8f81faec35508647ced65c44fd3e8648a5518afb", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8cb4b66379fb8..b1d3f01eb6818 100644 --- a/flake.nix +++ b/flake.nix @@ -21,57 +21,124 @@ ... }: let lib = nixpkgs.lib; + ncl = nci.lib.nci-lib; mkRootPath = rel: builtins.path { path = "${toString ./.}/${rel}"; name = rel; }; + filteredSource = let + pathsToIgnore = [ + ".envrc" + ".ignore" + ".github" + "runtime" + "screenshot.png" + "book" + "contrib" + "docs" + "README.md" + "CHANGELOG.md" + "shell.nix" + "default.nix" + "grammars.nix" + "flake.nix" + "flake.lock" + ]; + ignorePaths = path: type: let + # split the nix store path into its components + components = lib.splitString "/" path; + # drop off the `/nix/hash-source` section from the path + relPathComponents = lib.drop 4 components; + # reassemble the path components + relPath = lib.concatStringsSep "/" relPathComponents; + in + lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore; + in + builtins.path { + name = "helix-source"; + path = toString ./.; + # filter out unnecessary paths + filter = ignorePaths; + }; outputs = nci.lib.makeOutputs { root = ./.; - renameOutputs = {"helix-term" = "helix";}; - # Set default app to hx (binary is from helix-term release build) - # Set default package to helix-term release build - defaultOutputs = { - app = "hx"; - package = "helix"; + config = common: { + outputs = { + # rename helix-term to helix since it's our main package + rename = {"helix-term" = "helix";}; + # Set default app to hx (binary is from helix-term release build) + # Set default package to helix-term release build + defaults = { + app = "hx"; + package = "helix"; + }; + }; + cCompiler.package = with common.pkgs; + if stdenv.isLinux + then gcc + else clang; + shell = { + packages = with common.pkgs; + [lld_13 cargo-flamegraph rust-analyzer] + ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin) + ++ (lib.optional stdenv.isLinux lldb); + env = [ + { + name = "HELIX_RUNTIME"; + eval = "$PWD/runtime"; + } + { + name = "RUST_BACKTRACE"; + value = "1"; + } + { + name = "RUSTFLAGS"; + value = + if common.pkgs.stdenv.isLinux + then "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment" + else ""; + } + ]; + }; }; - overrides = { - cCompiler = common: - with common.pkgs; - if stdenv.isLinux - then gcc - else clang; - crateOverrides = common: _: { - helix-term = prev: { - src = builtins.path { - name = "helix-source"; - path = toString ./.; - # filter out unneeded stuff that cause rebuilds - filter = path: type: - lib.all - (n: builtins.baseNameOf path != n) - [ - ".envrc" - ".ignore" - ".github" - "runtime" - "screenshot.png" - "book" - "contrib" - "docs" - "README.md" - "shell.nix" - "default.nix" - "grammars.nix" - "flake.nix" - "flake.lock" - ]; - }; + pkgConfig = common: { + helix-term = { + # Wrap helix with runtime + wrapper = _: old: let + inherit (common) pkgs; + makeOverridableHelix = old: config: let + grammars = pkgs.callPackage ./grammars.nix config; + runtimeDir = pkgs.runCommand "helix-runtime" {} '' + mkdir -p $out + ln -s ${mkRootPath "runtime"}/* $out + rm -r $out/grammars + ln -s ${grammars} $out/grammars + ''; + helix-wrapped = + common.internal.pkgsSet.utils.wrapDerivation old + { + nativeBuildInputs = [pkgs.makeWrapper]; + makeWrapperArgs = config.makeWrapperArgs or []; + } + '' + rm -rf $out/bin + mkdir -p $out/bin + ln -sf ${old}/bin/* $out/bin/ + wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}" + ''; + in + helix-wrapped + // {override = makeOverridableHelix old;}; + in + makeOverridableHelix old {}; + overrides.fix-build.overrideAttrs = prev: { + src = filteredSource; # disable fetching and building of tree-sitter grammars in the helix-term build.rs HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1"; - buildInputs = (prev.buildInputs or []) ++ [common.cCompiler.cc.lib]; + buildInputs = ncl.addBuildInputs prev [common.config.cCompiler.package.cc.lib]; # link languages and theme toml files since helix-term expects them (for tests) preConfigure = '' @@ -87,88 +154,20 @@ meta.mainProgram = "hx"; }; }; - shell = common: prev: { - packages = - prev.packages - ++ ( - with common.pkgs; - [lld_13 cargo-flamegraph rust-analyzer] - ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin) - ++ (lib.optional stdenv.isLinux lldb) - ); - env = - prev.env - ++ [ - { - name = "HELIX_RUNTIME"; - eval = "$PWD/runtime"; - } - { - name = "RUST_BACKTRACE"; - value = "1"; - } - { - name = "RUSTFLAGS"; - value = - if common.pkgs.stdenv.isLinux - then "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment" - else ""; - } - ]; - }; }; }; - makeOverridableHelix = system: old: config: let - pkgs = nixpkgs.legacyPackages.${system}; - grammars = pkgs.callPackage ./grammars.nix config; - runtimeDir = pkgs.runCommand "helix-runtime" {} '' - mkdir -p $out - ln -s ${mkRootPath "runtime"}/* $out - rm -r $out/grammars - ln -s ${grammars} $out/grammars - ''; - helix-wrapped = - pkgs.runCommand "${old.name}-wrapped" - { - inherit (old) pname version meta; - - nativeBuildInputs = [pkgs.makeWrapper]; - makeWrapperArgs = config.makeWrapperArgs or []; - } - '' - mkdir -p $out - cp -r --no-preserve=mode,ownership ${old}/* $out/ - chmod +x $out/bin/* - wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}" - ''; - in - helix-wrapped - // {override = makeOverridableHelix system old;}; in outputs // { - apps = - lib.mapAttrs - ( - system: apps: rec { - default = hx; - hx = { - type = "app"; - program = lib.getExe self.${system}.packages.helix; - }; - } - ) - outputs.apps; packages = lib.mapAttrs ( - system: packages: rec { - default = helix; - helix = makeOverridableHelix system helix-unwrapped {}; - helix-debug = makeOverridableHelix system helix-unwrapped-debug {}; - helix-unwrapped = packages.helix; - helix-unwrapped-debug = packages.helix-debug; - } + system: packages: + packages + // { + helix-unwrapped = packages.helix.passthru.unwrapped; + helix-unwrapped-debug = packages.helix-debug.passthru.unwrapped; + } ) outputs.packages; };