From 7961355ba1c0cd521372496c507a31a51b41ddf2 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Wed, 24 Nov 2021 12:17:41 +0530 Subject: [PATCH 001/303] Change cursor shape on mode change Fixes #323. Due to terminal limitations we can only change the shape of the primary cursor. --- book/src/configuration.md | 42 +++++++++++++++++++++++++++++++++++-- helix-term/src/ui/editor.rs | 12 ++++++++--- helix-view/src/editor.rs | 42 +++++++++++++++++++++++++++++++------ helix-view/src/graphics.rs | 32 ++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 11 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 2ed48d51f..a40a89591 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,9 +5,26 @@ # Configuration * Linux and Mac: `~/.config/helix/config.toml` * Windows: `%AppData%\helix\config.toml` +Example config: + +```toml +theme = "onedark" + +[editor] +line-number = "relative" +mouse = false + +[editor.cursor-shape] +normal = "underline" +insert = "block" + +[editor.file-picker] +hidden = false +``` + ## Editor -`[editor]` section of the config. +### `[editor]` Section | Key | Description | Default | |--|--|---------| @@ -24,7 +41,28 @@ ## Editor | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `auto-info` | Whether to display infoboxes | `true` | -`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default. +### `[editor.cursor-shape]` Section + +Defines the shape of cursor in each mode. Note that due to limitations +of the terminal environment, only the primary cursor can change shape. + +| Key | Description | Default | +| --- | ----------- | -------- | +| `normal` | Cursor shape in [normal mode][normal mode] | `block` | +| `insert` | Cursor shape in [insert mode][insert mode] | `bar` | +| `select` | Cursor shape in [select mode][select mode] | `underline` | + +[normal mode]: ./keymap.md#normal-mode +[insert mode]: ./keymap.md#insert-mode +[select mode]: ./keymap.md#select--extend-mode + +### `[editor.filepicker]` Section + +Sets options for file picker and global search. All but the last key listed in +the default file-picker configuration below are IgnoreOptions: whether hidden +files and files listed within ignore files are ignored by (not visible in) the +helix file picker and global search. There is also one other key, `max-depth` +available, which is not defined by default. | Key | Description | Default | |--|--|---------| diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 27d33d225..8ad54dbdd 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -250,7 +250,9 @@ pub fn doc_selection_highlights( // Special-case: cursor at end of the rope. if range.head == range.anchor && range.head == text.len_chars() { - spans.push((cursor_scope, range.head..range.head + 1)); + if i != primary_idx { + spans.push((cursor_scope, range.head..range.head + 1)); + } continue; } @@ -259,11 +261,15 @@ pub fn doc_selection_highlights( // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); spans.push((selection_scope, range.anchor..cursor_start)); - spans.push((cursor_scope, cursor_start..range.head)); + if i != primary_idx { + spans.push((cursor_scope, cursor_start..range.head)); + } } else { // Reverse case. let cursor_end = next_grapheme_boundary(text, range.head); - spans.push((cursor_scope, range.head..cursor_end)); + if i != primary_idx { + spans.push((cursor_scope, range.head..cursor_end)); + } spans.push((selection_scope, cursor_end..range.anchor)); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1ce33760e..9c77f2705 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::SCRATCH_BUFFER_NAME, + document::{Mode, SCRATCH_BUFFER_NAME}, graphics::{CursorKind, Rect}, theme::{self, Theme}, tree::{self, Tree}, @@ -9,7 +9,7 @@ use futures_util::future; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, io::stdin, path::{Path, PathBuf}, pin::Pin, @@ -22,7 +22,7 @@ pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; -use helix_core::syntax; +use helix_core::{hashmap, syntax}; use helix_core::{Position, Selection}; use serde::Deserialize; @@ -103,6 +103,30 @@ pub struct Config { /// Whether to display infoboxes. Defaults to true. pub auto_info: bool, pub file_picker: FilePickerConfig, + /// Shape for cursor in each mode + pub cursor_shape: CursorShapeConfig, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(transparent)] +pub struct CursorShapeConfig(HashMap); + +impl std::ops::Deref for CursorShapeConfig { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for CursorShapeConfig { + fn default() -> Self { + Self(hashmap!( + Mode::Insert => CursorKind::Bar, + Mode::Normal => CursorKind::Block, + Mode::Select => CursorKind::Underline, + )) + } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -110,7 +134,6 @@ pub struct Config { pub enum LineNumber { /// Show absolute line number Absolute, - /// Show relative line number to the primary cursor Relative, } @@ -135,6 +158,7 @@ fn default() -> Self { completion_trigger_len: 2, auto_info: true, file_picker: FilePickerConfig::default(), + cursor_shape: CursorShapeConfig::default(), } } } @@ -594,9 +618,15 @@ pub fn cursor(&self) -> (Option, CursorKind) { let inner = view.inner_area(); pos.col += inner.x as usize; pos.row += inner.y as usize; - (Some(pos), CursorKind::Hidden) + let cursorkind = self + .config + .cursor_shape + .get(&doc.mode()) + .copied() + .unwrap_or_default(); + (Some(pos), cursorkind) } else { - (None, CursorKind::Hidden) + (None, CursorKind::default()) } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 0bfca04aa..c9dd21e30 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -1,4 +1,6 @@ +use anyhow::{anyhow, Error}; use bitflags::bitflags; +use serde::de::{self, Deserialize, Deserializer}; use std::{ cmp::{max, min}, str::FromStr, @@ -17,6 +19,36 @@ pub enum CursorKind { Hidden, } +impl Default for CursorKind { + fn default() -> Self { + Self::Block + } +} + +impl FromStr for CursorKind { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "bar" => Ok(Self::Bar), + "block" => Ok(Self::Block), + "underline" => Ok(Self::Underline), + _ => Err(anyhow!("Invalid cursor '{}'", s)), + } + } +} + +// toml deserializer doesn't seem to recognize string as enum +impl<'de> Deserialize<'de> for CursorKind { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Margin { pub vertical: u16, From 17473b51d37001d4966b82d83d61d3e695e8403d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Thu, 25 Nov 2021 22:35:07 +0530 Subject: [PATCH 002/303] Use serde attribute to rename to lowercase --- helix-view/src/graphics.rs | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index c9dd21e30..acdaa6961 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -1,12 +1,12 @@ -use anyhow::{anyhow, Error}; use bitflags::bitflags; -use serde::de::{self, Deserialize, Deserializer}; +use serde::Deserialize; use std::{ cmp::{max, min}, str::FromStr, }; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[serde(rename_all = "lowercase")] /// UNSTABLE pub enum CursorKind { /// █ @@ -25,30 +25,6 @@ fn default() -> Self { } } -impl FromStr for CursorKind { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s { - "bar" => Ok(Self::Bar), - "block" => Ok(Self::Block), - "underline" => Ok(Self::Underline), - _ => Err(anyhow!("Invalid cursor '{}'", s)), - } - } -} - -// toml deserializer doesn't seem to recognize string as enum -impl<'de> Deserialize<'de> for CursorKind { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - s.parse().map_err(de::Error::custom) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Margin { pub vertical: u16, From 058796c18e786309322731ff68b15a0f3901b60b Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Mon, 29 Nov 2021 11:09:04 +0530 Subject: [PATCH 003/303] Change default cursors to block for all modes --- book/src/configuration.md | 15 ++++++++------- helix-view/src/editor.rs | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index a40a89591..2998bcdcb 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -15,8 +15,9 @@ # Configuration mouse = false [editor.cursor-shape] -normal = "underline" -insert = "block" +insert = "bar" +normal = "block" +select = "underline" [editor.file-picker] hidden = false @@ -46,11 +47,11 @@ ### `[editor.cursor-shape]` Section Defines the shape of cursor in each mode. Note that due to limitations of the terminal environment, only the primary cursor can change shape. -| Key | Description | Default | -| --- | ----------- | -------- | -| `normal` | Cursor shape in [normal mode][normal mode] | `block` | -| `insert` | Cursor shape in [insert mode][insert mode] | `bar` | -| `select` | Cursor shape in [select mode][select mode] | `underline` | +| Key | Description | Default | +| --- | ----------- | ------- | +| `normal` | Cursor shape in [normal mode][normal mode] | `block` | +| `insert` | Cursor shape in [insert mode][insert mode] | `block` | +| `select` | Cursor shape in [select mode][select mode] | `block` | [normal mode]: ./keymap.md#normal-mode [insert mode]: ./keymap.md#insert-mode diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9c77f2705..b558c183d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -122,9 +122,9 @@ fn deref(&self) -> &Self::Target { impl Default for CursorShapeConfig { fn default() -> Self { Self(hashmap!( - Mode::Insert => CursorKind::Bar, + Mode::Insert => CursorKind::Block, Mode::Normal => CursorKind::Block, - Mode::Select => CursorKind::Underline, + Mode::Select => CursorKind::Block, )) } } From c08d2fae587a0a5dd2a1e2e44a1f385d142c9d59 Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Tue, 30 Nov 2021 15:40:38 +0800 Subject: [PATCH 004/303] Improve dedent behavior, make kill_to_line_end behave like emacs (#1173) * restore indent when press esc right after open a new line * add comment for restore_indent * fix, and make kill to line end behaves like emacs * update comment * fix comment * adjust cancel restore_indent situation * check esc logic in mode transaction * improve comment * add more check for dedent * update comment * use matches to check for last_cmd * no need to introduct CommandFun type --- helix-term/src/commands.rs | 19 +++++++++++++++---- helix-term/src/ui/editor.rs | 13 +++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b4cc9ae92..0a2ecf9ca 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -173,6 +173,10 @@ pub fn doc(&self) -> &'static str { self.doc } + pub fn fun(&self) -> fn(&mut Context) { + self.fun + } + #[rustfmt::skip] commands!( no_op, "Do nothing", @@ -603,8 +607,15 @@ fn kill_to_line_end(cx: &mut Context) { let selection = doc.selection(view.id).clone().transform(|range| { let line = range.cursor_line(text); - let pos = line_end_char_index(&text, line); - range.put_cursor(text, pos, true) + let line_end_pos = line_end_char_index(&text, line); + let pos = range.cursor(text); + + let mut new_range = range.put_cursor(text, line_end_pos, true); + // don't want to remove the line separator itself if the cursor doesn't reach the end of line. + if pos != line_end_pos { + new_range.head = line_end_pos; + } + new_range }); delete_selection_insert_mode(doc, view, &selection); } @@ -3503,12 +3514,12 @@ fn open(cx: &mut Context, open: Open) { } // o inserts a new line after each line with a selection -fn open_below(cx: &mut Context) { +pub(crate) fn open_below(cx: &mut Context) { open(cx, Open::Below) } // O inserts a new line before each line with a selection -fn open_above(cx: &mut Context) { +pub(crate) fn open_above(cx: &mut Context) { open(cx, Open::Above) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 83be816f9..c6d0e71f2 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1023,6 +1023,19 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { (Mode::Insert, Mode::Normal) => { // if exiting insert mode, remove completion self.completion = None; + + let last_cmd = self.last_insert.0.fun(); + const OPEN_BELOW_FUN: fn(&mut commands::Context) = commands::open_below; + const OPEN_ABOVE_FUN: fn(&mut commands::Context) = commands::open_above; + // For user friendly, + // Remove whitespaces if we go from insert mode(through open below/above) to normal mode without any keys in between. + // Example: `o`. + if matches!(last_cmd, OPEN_BELOW_FUN | OPEN_ABOVE_FUN) + && self.last_insert.1.len() == 1 + { + commands::Command::goto_line_start.execute(&mut cxt); + commands::Command::kill_to_line_end.execute(&mut cxt); + } } _ => (), } From 3e15aead4adc5139417230d15b38112cdc4f7043 Mon Sep 17 00:00:00 2001 From: George Rodrigues Date: Tue, 30 Nov 2021 21:11:25 -0300 Subject: [PATCH 005/303] Fix typo on docs (#1201) --- book/src/guides/adding_languages.md | 2 +- helix-core/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 446eb479d..9ad2c2859 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -2,7 +2,7 @@ # Adding languages ## Submodules -To add a new langauge, you should first add a tree-sitter submodule. To do this, +To add a new language, you should first add a tree-sitter submodule. To do this, you can run the command ```sh git submodule add -f helix-syntax/languages/tree-sitter- diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index de7e95c16..8ef41ef33 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -158,7 +158,7 @@ fn language_tomls() { "; let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) - .expect("Couldn't parse built-in langauges config"); + .expect("Couldn't parse built-in languages config"); let user: Value = toml::from_str(USER).unwrap(); let merged = merge_toml_values(base, user); From d562e13e1f875b1f5675ddc7ac6766a04228caaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 30 Nov 2021 12:59:19 +0900 Subject: [PATCH 006/303] minor: Use anchor::ensure in some cases --- helix-term/src/commands.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0a2ecf9ca..f938b719a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -24,7 +24,7 @@ Document, DocumentId, Editor, ViewId, }; -use anyhow::{anyhow, bail, Context as _}; +use anyhow::{anyhow, bail, ensure, Context as _}; use helix_lsp::{ block_on, lsp, util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range}, @@ -2550,9 +2550,7 @@ pub(super) fn goto_line_number( args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - if args.is_empty() { - bail!("Line number required"); - } + ensure!(!args.is_empty(), "Line number required"); let line = args[0].parse::()?; @@ -5553,9 +5551,7 @@ fn shell_impl( ) -> anyhow::Result<(Tendril, bool)> { use std::io::Write; use std::process::{Command, Stdio}; - if shell.is_empty() { - bail!("No shell set"); - } + ensure!(!shell.is_empty(), "No shell set"); let mut process = match Command::new(&shell[0]) .args(&shell[1..]) From 7bbf4c5b0675bb794bd88c070472773bd7d7e6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 1 Dec 2021 12:57:57 +0900 Subject: [PATCH 007/303] ui: Only calculate span styling when it's actually in bounds --- helix-term/src/ui/editor.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index c6d0e71f2..6299014cc 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -304,17 +304,17 @@ pub fn render_text_highlights>( use helix_core::graphemes::{grapheme_width, RopeGraphemes}; - let style = spans.iter().fold(text_style, |acc, span| { - let style = theme.get(theme.scopes()[span.0].as_str()); - acc.patch(style) - }); - for grapheme in RopeGraphemes::new(text) { let out_of_bounds = visual_x < offset.col as u16 || visual_x >= viewport.width + offset.col as u16; if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { + let style = spans.iter().fold(text_style, |acc, span| { + let style = theme.get(theme.scopes()[span.0].as_str()); + acc.patch(style) + }); + // we still want to render an empty cell with the style surface.set_string( viewport.x + visual_x - offset.col as u16, @@ -345,6 +345,11 @@ pub fn render_text_highlights>( }; if !out_of_bounds { + let style = spans.iter().fold(text_style, |acc, span| { + let style = theme.get(theme.scopes()[span.0].as_str()); + acc.patch(style) + }); + // if we're offscreen just keep going until we hit a new line surface.set_string( viewport.x + visual_x - offset.col as u16, From 259678585c3410044683bf4d2619b2d024a04514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Wed, 1 Dec 2021 13:08:20 +0900 Subject: [PATCH 008/303] ui: Optimize tree-sitter style lookups Tree sitter returns an index referring to the position of the scope in the scopes array. We can use that same index to avoid a hashmap lookup and instead store the styles in an array. This currently stores the styles in both a map and an array because the UI still uses hashmap lookups, but it's a reasonable tradeoff. --- helix-term/src/ui/editor.rs | 6 ++---- helix-view/src/theme.rs | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6299014cc..a4fbca990 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -311,8 +311,7 @@ pub fn render_text_highlights>( if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { let style = spans.iter().fold(text_style, |acc, span| { - let style = theme.get(theme.scopes()[span.0].as_str()); - acc.patch(style) + acc.patch(theme.highlight(span.0)) }); // we still want to render an empty cell with the style @@ -346,8 +345,7 @@ pub fn render_text_highlights>( if !out_of_bounds { let style = spans.iter().fold(text_style, |acc, span| { - let style = theme.get(theme.scopes()[span.0].as_str()); - acc.patch(style) + acc.patch(theme.highlight(span.0)) }); // if we're offscreen just keep going until we hit a new line diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 757316bde..2366ff7d1 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -78,8 +78,11 @@ pub fn default(&self) -> Theme { #[derive(Clone, Debug)] pub struct Theme { - scopes: Vec, + // UI styles are stored in a HashMap styles: HashMap, + // tree-sitter highlight styles are stored in a Vec to optimize lookups + scopes: Vec, + highlights: Vec