on master help sign in
Merge pull request #1154 from sudormrfbin/cursor-shape-new

Change cursor shape on mode change

commite2d2f19fd02419b8d144ae7a8a359d31bd773af7 authorBlaž Hrastnik <blaz@mxxn.io> dateSun 23 Jan 2022, 16:35 (5 years ago) parenta8e69e1b3b4e78
5 files changed+155−23
book/src/configuration.md+41−2 helix-term/src/ui/editor.rs+29−11 helix-view/src/document.rs+13−3 helix-view/src/editor.rs+63−6 helix-view/src/graphics.rs+9−1
book/src/configuration.md+41−2
@@ -5,9 +5,27 @@ To override global configuration parameters, create a `config.toml` file located
55* Linux and Mac: `~/.config/helix/config.toml`
66* Windows: `%AppData%\helix\config.toml`
77
8+Example config:
9+
10+```toml
11+theme = "onedark"
12+
13+[editor]
14+line-number = "relative"
15+mouse = false
16+
17+[editor.cursor-shape]
18+insert = "bar"
19+normal = "block"
20+select = "underline"
21+
22+[editor.file-picker]
23+hidden = false
24+```
25+
826## Editor
927
10−`[editor]` section of the config.
28+### `[editor]` Section
1129
1230| Key | Description | Default |
1331|--|--|---------|
@@ -25,7 +43,28 @@ To override global configuration parameters, create a `config.toml` file located
2543| `auto-info` | Whether to display infoboxes | `true` |
2644| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
2745
28−`[editor.file-picker]` 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.
46+### `[editor.cursor-shape]` Section
47+
48+Defines the shape of cursor in each mode. Note that due to limitations
49+of the terminal environment, only the primary cursor can change shape.
50+
51+| Key | Description | Default |
52+| --- | ----------- | ------- |
53+| `normal` | Cursor shape in [normal mode][normal mode] | `block` |
54+| `insert` | Cursor shape in [insert mode][insert mode] | `block` |
55+| `select` | Cursor shape in [select mode][select mode] | `block` |
56+
57+[normal mode]: ./keymap.md#normal-mode
58+[insert mode]: ./keymap.md#insert-mode
59+[select mode]: ./keymap.md#select--extend-mode
60+
61+### `[editor.file-picker]` Section
62+
63+Sets options for file picker and global search. All but the last key listed in
64+the default file-picker configuration below are IgnoreOptions: whether hidden
65+files and files listed within ignore files are ignored by (not visible in) the
66+helix file picker and global search. There is also one other key, `max-depth`
67+available, which is not defined by default.
2968
3069| Key | Description | Default |
3170|--|--|---------|
helix-term/src/ui/editor.rs+29−11
@@ -19,6 +19,7 @@ use helix_core::{
1919};
2020use helix_view::{
2121 document::{Mode, SCRATCH_BUFFER_NAME},
22+ editor::CursorShapeConfig,
2223 graphics::{CursorKind, Modifier, Rect, Style},
2324 info::Info,
2425 input::KeyEvent,
@@ -80,7 +81,7 @@ impl EditorView {
8081 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
8182 Box::new(syntax::merge(
8283 highlights,
83− Self::doc_selection_highlights(doc, view, theme),
84+ Self::doc_selection_highlights(doc, view, theme, &config.cursor_shape),
8485 ))
8586 } else {
8687 Box::new(highlights)
@@ -195,11 +196,16 @@ impl EditorView {
195196 doc: &Document,
196197 view: &View,
197198 theme: &Theme,
199+ cursor_shape_config: &CursorShapeConfig,
198200 ) -> Vec<(usize, std::ops::Range<usize>)> {
199201 let text = doc.text().slice(..);
200202 let selection = doc.selection(view.id);
201203 let primary_idx = selection.primary_index();
202204
205+ let mode = doc.mode();
206+ let cursorkind = cursor_shape_config.from_mode(mode);
207+ let cursor_is_block = cursorkind == CursorKind::Block;
208+
203209 let selection_scope = theme
204210 .find_scope_index("ui.selection")
205211 .expect("could not find `ui.selection` scope in the theme!");
@@ -207,7 +213,7 @@ impl EditorView {
207213 .find_scope_index("ui.cursor")
208214 .unwrap_or(selection_scope);
209215
210− let cursor_scope = match doc.mode() {
216+ let cursor_scope = match mode {
211217 Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
212218 Mode::Select => theme.find_scope_index("ui.cursor.select"),
213219 Mode::Normal => Some(base_cursor_scope),
@@ -223,7 +229,8 @@ impl EditorView {
223229
224230 let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
225231 for (i, range) in selection.iter().enumerate() {
226− let (cursor_scope, selection_scope) = if i == primary_idx {
232+ let selection_is_primary = i == primary_idx;
233+ let (cursor_scope, selection_scope) = if selection_is_primary {
227234 (primary_cursor_scope, primary_selection_scope)
228235 } else {
229236 (cursor_scope, selection_scope)
@@ -231,7 +238,14 @@ impl EditorView {
231238
232239 // Special-case: cursor at end of the rope.
233240 if range.head == range.anchor && range.head == text.len_chars() {
234− spans.push((cursor_scope, range.head..range.head + 1));
241+ if !selection_is_primary || cursor_is_block {
242+ // Bar and underline cursors are drawn by the terminal
243+ // BUG: If the editor area loses focus while having a bar or
244+ // underline cursor (eg. when a regex prompt has focus) then
245+ // the primary cursor will be invisible. This doesn't happen
246+ // with block cursors since we manually draw *all* cursors.
247+ spans.push((cursor_scope, range.head..range.head + 1));
248+ }
235249 continue;
236250 }
237251
@@ -240,11 +254,15 @@ impl EditorView {
240254 // Standard case.
241255 let cursor_start = prev_grapheme_boundary(text, range.head);
242256 spans.push((selection_scope, range.anchor..cursor_start));
243− spans.push((cursor_scope, cursor_start..range.head));
257+ if !selection_is_primary || cursor_is_block {
258+ spans.push((cursor_scope, cursor_start..range.head));
259+ }
244260 } else {
245261 // Reverse case.
246262 let cursor_end = next_grapheme_boundary(text, range.head);
247− spans.push((cursor_scope, range.head..cursor_end));
263+ if !selection_is_primary || cursor_is_block {
264+ spans.push((cursor_scope, range.head..cursor_end));
265+ }
248266 spans.push((selection_scope, cursor_end..range.anchor));
249267 }
250268 }
@@ -1141,11 +1159,11 @@ impl Component for EditorView {
11411159 }
11421160
11431161 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1144− // match view.doc.mode() {
1145− // Mode::Insert => write!(stdout, "\x1B[6 q"),
1146− // mode => write!(stdout, "\x1B[2 q"),
1147− // };
1148− editor.cursor()
1162+ match editor.cursor() {
1163+ // All block cursors are drawn manually
1164+ (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
1165+ cursor => cursor,
1166+ }
11491167 }
11501168}
11511169
helix-view/src/document.rs+13−3
@@ -1,5 +1,6 @@
11use anyhow::{anyhow, Context, Error};
22use serde::de::{self, Deserialize, Deserializer};
3+use serde::Serialize;
34use std::cell::Cell;
45use std::collections::HashMap;
56use std::fmt::Display;
@@ -30,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
3031
3132#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
3233pub enum Mode {
33− Normal,
34− Select,
35− Insert,
34+ Normal = 0,
35+ Select = 1,
36+ Insert = 2,
3637}
3738
3839impl Display for Mode {
@@ -69,6 +70,15 @@ impl<'de> Deserialize<'de> for Mode {
6970 }
7071}
7172
73+impl Serialize for Mode {
74+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75+ where
76+ S: serde::Serializer,
77+ {
78+ serializer.collect_str(self)
79+ }
80+}
81+
7282pub struct Document {
7383 pub(crate) id: DocumentId,
7484 text: Rope,
helix-view/src/editor.rs+63−6
@@ -1,6 +1,6 @@
11use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
3− document::SCRATCH_BUFFER_NAME,
3+ document::{Mode, SCRATCH_BUFFER_NAME},
44 graphics::{CursorKind, Rect},
55 input::KeyEvent,
66 theme::{self, Theme},
@@ -10,7 +10,7 @@ use crate::{
1010
1111use futures_util::future;
1212use std::{
13− collections::BTreeMap,
13+ collections::{BTreeMap, HashMap},
1414 io::stdin,
1515 num::NonZeroUsize,
1616 path::{Path, PathBuf},
@@ -27,7 +27,7 @@ pub use helix_core::register::Registers;
2727use helix_core::syntax;
2828use helix_core::{Position, Selection};
2929
30−use serde::{Deserialize, Serialize};
30+use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
3131
3232fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
3333where
@@ -105,16 +105,71 @@ pub struct Config {
105105 /// Whether to display infoboxes. Defaults to true.
106106 pub auto_info: bool,
107107 pub file_picker: FilePickerConfig,
108+ /// Shape for cursor in each mode
109+ pub cursor_shape: CursorShapeConfig,
108110 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
109111 pub true_color: bool,
110112}
111113
114+// Cursor shape is read and used on every rendered frame and so needs
115+// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
116+#[derive(Debug, Clone, PartialEq)]
117+pub struct CursorShapeConfig([CursorKind; 3]);
118+
119+impl CursorShapeConfig {
120+ pub fn from_mode(&self, mode: Mode) -> CursorKind {
121+ self.get(mode as usize).copied().unwrap_or_default()
122+ }
123+}
124+
125+impl<'de> Deserialize<'de> for CursorShapeConfig {
126+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127+ where
128+ D: Deserializer<'de>,
129+ {
130+ let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
131+ let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
132+ Ok(CursorShapeConfig([
133+ into_cursor(Mode::Normal),
134+ into_cursor(Mode::Select),
135+ into_cursor(Mode::Insert),
136+ ]))
137+ }
138+}
139+
140+impl Serialize for CursorShapeConfig {
141+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
142+ where
143+ S: serde::Serializer,
144+ {
145+ let mut map = serializer.serialize_map(Some(self.len()))?;
146+ let modes = [Mode::Normal, Mode::Select, Mode::Insert];
147+ for mode in modes {
148+ map.serialize_entry(&mode, &self.from_mode(mode))?;
149+ }
150+ map.end()
151+ }
152+}
153+
154+impl std::ops::Deref for CursorShapeConfig {
155+ type Target = [CursorKind; 3];
156+
157+ fn deref(&self) -> &Self::Target {
158+ &self.0
159+ }
160+}
161+
162+impl Default for CursorShapeConfig {
163+ fn default() -> Self {
164+ Self([CursorKind::Block; 3])
165+ }
166+}
167+
112168#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
113169#[serde(rename_all = "kebab-case")]
114170pub enum LineNumber {
115171 /// Show absolute line number
116172 Absolute,
117−
118173 /// Show relative line number to the primary cursor
119174 Relative,
120175}
@@ -151,6 +206,7 @@ impl Default for Config {
151206 completion_trigger_len: 2,
152207 auto_info: true,
153208 file_picker: FilePickerConfig::default(),
209+ cursor_shape: CursorShapeConfig::default(),
154210 true_color: false,
155211 }
156212 }
@@ -614,9 +670,10 @@ impl Editor {
614670 let inner = view.inner_area();
615671 pos.col += inner.x as usize;
616672 pos.row += inner.y as usize;
617− (Some(pos), CursorKind::Hidden)
673+ let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
674+ (Some(pos), cursorkind)
618675 } else {
619− (None, CursorKind::Hidden)
676+ (None, CursorKind::default())
620677 }
621678 }
622679
helix-view/src/graphics.rs+9−1
@@ -1,10 +1,12 @@
11use bitflags::bitflags;
2+use serde::{Deserialize, Serialize};
23use std::{
34 cmp::{max, min},
45 str::FromStr,
56};
67
7−#[derive(Debug, Clone, Copy, PartialEq)]
8+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
9+#[serde(rename_all = "lowercase")]
810/// UNSTABLE
911pub enum CursorKind {
1012 /// █
@@ -17,6 +19,12 @@ pub enum CursorKind {
1719 Hidden,
1820}
1921
22+impl Default for CursorKind {
23+ fn default() -> Self {
24+ Self::Block
25+ }
26+}
27+
2028#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2129pub struct Margin {
2230 pub vertical: u16,