From 7961355ba1c0cd521372496c507a31a51b41ddf2 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Wed, 24 Nov 2021 12:17:41 +0530 Subject: [PATCH 1/5] 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 2ed48d51..a40a8959 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,9 +5,26 @@ To override global configuration parameters, create a `config.toml` file located * 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 @@ To override global configuration parameters, create a `config.toml` file located | `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 27d33d22..8ad54dbd 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -250,7 +250,9 @@ impl EditorView { // 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 @@ impl EditorView { // 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 1ce33760..9c77f270 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 crate::{ use futures_util::future; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, io::stdin, path::{Path, PathBuf}, pin::Pin, @@ -22,7 +22,7 @@ use anyhow::Error; 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 @@ impl Default for Config { completion_trigger_len: 2, auto_info: true, file_picker: FilePickerConfig::default(), + cursor_shape: CursorShapeConfig::default(), } } } @@ -594,9 +618,15 @@ impl Editor { 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 0bfca04a..c9dd21e3 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 2/5] 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 c9dd21e3..acdaa696 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 @@ impl Default for CursorKind { } } -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 3/5] 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 a40a8959..2998bcdc 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -15,8 +15,9 @@ line-number = "relative" mouse = false [editor.cursor-shape] -normal = "underline" -insert = "block" +insert = "bar" +normal = "block" +select = "underline" [editor.file-picker] hidden = false @@ -46,11 +47,11 @@ hidden = false 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 9c77f270..b558c183 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -122,9 +122,9 @@ impl std::ops::Deref for CursorShapeConfig { 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 016640f4fb6f620df13a2cab15e749d623197a51 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 18 Dec 2021 08:25:40 +0530 Subject: [PATCH 4/5] Remove ui.cursor.primary and hashmap lookups --- book/src/themes.md | 1 - helix-term/src/ui/editor.rs | 17 ++++++++--------- helix-view/src/document.rs | 6 +++--- helix-view/src/editor.rs | 36 ++++++++++++++++++++++++------------ 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index ecbbb6e9..6b38fb43 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -173,7 +173,6 @@ These scopes are used for theming the editor interface. | `ui.cursor.insert` | | | `ui.cursor.select` | | | `ui.cursor.match` | Matching bracket etc. | -| `ui.cursor.primary` | Cursor with primary selection | | `ui.linenr` | | | `ui.linenr.selected` | | | `ui.statusline` | Statusline | diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 8ad54dbd..e8f8fd9b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -233,24 +233,23 @@ impl EditorView { } .unwrap_or(base_cursor_scope); - let primary_cursor_scope = theme - .find_scope_index("ui.cursor.primary") - .unwrap_or(cursor_scope); let primary_selection_scope = theme .find_scope_index("ui.selection.primary") .unwrap_or(selection_scope); let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); for (i, range) in selection.iter().enumerate() { - let (cursor_scope, selection_scope) = if i == primary_idx { - (primary_cursor_scope, primary_selection_scope) + let selection_is_primary = i == primary_idx; + let selection_scope = if selection_is_primary { + primary_selection_scope } else { - (cursor_scope, selection_scope) + selection_scope }; // Special-case: cursor at end of the rope. if range.head == range.anchor && range.head == text.len_chars() { - if i != primary_idx { + if !selection_is_primary { + // Terminal cursor acts as the primary cursor spans.push((cursor_scope, range.head..range.head + 1)); } continue; @@ -261,13 +260,13 @@ impl EditorView { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); spans.push((selection_scope, range.anchor..cursor_start)); - if i != primary_idx { + if !selection_is_primary { spans.push((cursor_scope, cursor_start..range.head)); } } else { // Reverse case. let cursor_end = next_grapheme_boundary(text, range.head); - if i != primary_idx { + if !selection_is_primary { spans.push((cursor_scope, range.head..cursor_end)); } spans.push((selection_scope, cursor_end..range.anchor)); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 76b19a07..01975452 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -29,9 +29,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { - Normal, - Select, - Insert, + Normal = 0, + Select = 1, + Insert = 2, } impl Display for Mode { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index b558c183..a121a836 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -22,10 +22,10 @@ use anyhow::Error; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; -use helix_core::{hashmap, syntax}; +use helix_core::syntax; use helix_core::{Position, Selection}; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result where @@ -107,12 +107,28 @@ pub struct Config { pub cursor_shape: CursorShapeConfig, } -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(transparent)] -pub struct CursorShapeConfig(HashMap); +// Cursor shape is read and used on every rendered frame and so needs +// to be fast. Therefore we avoid a hashmap and use an enum indexed array. +#[derive(Debug, Clone, PartialEq)] +pub struct CursorShapeConfig([CursorKind; 3]); + +impl<'de> Deserialize<'de> for CursorShapeConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let m = HashMap::::deserialize(deserializer)?; + let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default(); + Ok(CursorShapeConfig([ + into_cursor(Mode::Normal), + into_cursor(Mode::Select), + into_cursor(Mode::Insert), + ])) + } +} impl std::ops::Deref for CursorShapeConfig { - type Target = HashMap; + type Target = [CursorKind; 3]; fn deref(&self) -> &Self::Target { &self.0 @@ -121,11 +137,7 @@ impl std::ops::Deref for CursorShapeConfig { impl Default for CursorShapeConfig { fn default() -> Self { - Self(hashmap!( - Mode::Insert => CursorKind::Block, - Mode::Normal => CursorKind::Block, - Mode::Select => CursorKind::Block, - )) + Self([CursorKind::Block; 3]) } } @@ -621,7 +633,7 @@ impl Editor { let cursorkind = self .config .cursor_shape - .get(&doc.mode()) + .get(doc.mode() as usize) .copied() .unwrap_or_default(); (Some(pos), cursorkind) From c0bbadcaaf42698d102fa03f6f9267021f3efec0 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Thu, 23 Dec 2021 11:56:52 +0530 Subject: [PATCH 5/5] Manually draw all block cursors --- book/src/themes.md | 1 + helix-syntax/languages/tree-sitter-scala | 2 +- helix-term/src/ui/editor.rs | 41 ++++++++++++++++-------- helix-view/src/editor.rs | 13 ++++---- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 40c14781..b6de7002 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -188,6 +188,7 @@ These scopes are used for theming the editor interface. | `ui.cursor.insert` | | | `ui.cursor.select` | | | `ui.cursor.match` | Matching bracket etc. | +| `ui.cursor.primary` | Cursor with primary selection | | `ui.linenr` | | | `ui.linenr.selected` | | | `ui.statusline` | Statusline | diff --git a/helix-syntax/languages/tree-sitter-scala b/helix-syntax/languages/tree-sitter-scala index fb23ed9a..0a3dd53a 160000 --- a/helix-syntax/languages/tree-sitter-scala +++ b/helix-syntax/languages/tree-sitter-scala @@ -1 +1 @@ -Subproject commit fb23ed9a99da012d86b7a5059b9d8928607cce29 +Subproject commit 0a3dd53a7fc4b352a538397d054380aaa28be54c diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7d57e581..6b015171 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -17,6 +17,7 @@ use helix_core::{ }; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, + editor::CursorShapeConfig, graphics::{CursorKind, Modifier, Rect, Style}, info::Info, input::KeyEvent, @@ -79,7 +80,7 @@ impl EditorView { let highlights: Box> = if is_focused { Box::new(syntax::merge( highlights, - Self::doc_selection_highlights(doc, view, theme), + Self::doc_selection_highlights(doc, view, theme, &config.cursor_shape), )) } else { Box::new(highlights) @@ -213,11 +214,16 @@ impl EditorView { doc: &Document, view: &View, theme: &Theme, + cursor_shape_config: &CursorShapeConfig, ) -> Vec<(usize, std::ops::Range)> { let text = doc.text().slice(..); let selection = doc.selection(view.id); let primary_idx = selection.primary_index(); + let mode = doc.mode(); + let cursorkind = cursor_shape_config.from_mode(mode); + let cursor_is_block = cursorkind == CursorKind::Block; + let selection_scope = theme .find_scope_index("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); @@ -225,13 +231,16 @@ impl EditorView { .find_scope_index("ui.cursor") .unwrap_or(selection_scope); - let cursor_scope = match doc.mode() { + let cursor_scope = match mode { Mode::Insert => theme.find_scope_index("ui.cursor.insert"), Mode::Select => theme.find_scope_index("ui.cursor.select"), Mode::Normal => Some(base_cursor_scope), } .unwrap_or(base_cursor_scope); + let primary_cursor_scope = theme + .find_scope_index("ui.cursor.primary") + .unwrap_or(cursor_scope); let primary_selection_scope = theme .find_scope_index("ui.selection.primary") .unwrap_or(selection_scope); @@ -239,16 +248,20 @@ impl EditorView { let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); for (i, range) in selection.iter().enumerate() { let selection_is_primary = i == primary_idx; - let selection_scope = if selection_is_primary { - primary_selection_scope + let (cursor_scope, selection_scope) = if selection_is_primary { + (primary_cursor_scope, primary_selection_scope) } else { - selection_scope + (cursor_scope, selection_scope) }; // Special-case: cursor at end of the rope. if range.head == range.anchor && range.head == text.len_chars() { - if !selection_is_primary { - // Terminal cursor acts as the primary cursor + if !selection_is_primary || cursor_is_block { + // Bar and underline cursors are drawn by the terminal + // BUG: If the editor area loses focus while having a bar or + // underline cursor (eg. when a regex prompt has focus) then + // the primary cursor will be invisible. This doesn't happen + // with block cursors since we manually draw *all* cursors. spans.push((cursor_scope, range.head..range.head + 1)); } continue; @@ -259,13 +272,13 @@ impl EditorView { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); spans.push((selection_scope, range.anchor..cursor_start)); - if !selection_is_primary { + if !selection_is_primary || cursor_is_block { spans.push((cursor_scope, cursor_start..range.head)); } } else { // Reverse case. let cursor_end = next_grapheme_boundary(text, range.head); - if !selection_is_primary { + if !selection_is_primary || cursor_is_block { spans.push((cursor_scope, range.head..cursor_end)); } spans.push((selection_scope, cursor_end..range.anchor)); @@ -1140,11 +1153,11 @@ impl Component for EditorView { } fn cursor(&self, _area: Rect, editor: &Editor) -> (Option, CursorKind) { - // match view.doc.mode() { - // Mode::Insert => write!(stdout, "\x1B[6 q"), - // mode => write!(stdout, "\x1B[2 q"), - // }; - editor.cursor() + match editor.cursor() { + // All block cursors are drawn manually + (pos, CursorKind::Block) => (pos, CursorKind::Hidden), + cursor => cursor, + } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index fff4792d..d65c1fb2 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -116,6 +116,12 @@ pub struct Config { #[derive(Debug, Clone, PartialEq)] pub struct CursorShapeConfig([CursorKind; 3]); +impl CursorShapeConfig { + pub fn from_mode(&self, mode: Mode) -> CursorKind { + self.get(mode as usize).copied().unwrap_or_default() + } +} + impl<'de> Deserialize<'de> for CursorShapeConfig { fn deserialize(deserializer: D) -> Result where @@ -647,12 +653,7 @@ impl Editor { let inner = view.inner_area(); pos.col += inner.x as usize; pos.row += inner.y as usize; - let cursorkind = self - .config - .cursor_shape - .get(doc.mode() as usize) - .copied() - .unwrap_or_default(); + let cursorkind = self.config.cursor_shape.from_mode(doc.mode()); (Some(pos), cursorkind) } else { (None, CursorKind::default())