on master help sign in
Merge pull request #1154 from sudormrfbin/cursor-shape-new
e2d2f19Blaž Hrastnik
book/src/configuration.md+41−2
@@ -5,9 +5,27 @@ To override global configuration parameters, create a `config.toml` file located
5* Linux and Mac: `~/.config/helix/config.toml`
5* Linux and Mac: `~/.config/helix/config.toml`
6* Windows: `%AppData%\helix\config.toml`
6* Windows: `%AppData%\helix\config.toml`
7
7
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+
8## Editor
26## Editor
9
27
10−`[editor]` section of the config.
28+### `[editor]` Section
11
29
12| Key | Description | Default |
30| Key | Description | Default |
13|--|--|---------|
31|--|--|---------|
@@ -25,7 +43,28 @@ To override global configuration parameters, create a `config.toml` file located
25| `auto-info` | Whether to display infoboxes | `true` |
43| `auto-info` | Whether to display infoboxes | `true` |
26| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
44| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
27
45
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.
29
68
30| Key | Description | Default |
69| Key | Description | Default |
31|--|--|---------|
70|--|--|---------|
helix-term/src/ui/editor.rs+29−11
@@ -19,6 +19,7 @@ use helix_core::{
19};
19};
20use helix_view::{
20use helix_view::{
21 document::{Mode, SCRATCH_BUFFER_NAME},
21 document::{Mode, SCRATCH_BUFFER_NAME},
22+ editor::CursorShapeConfig,
22 graphics::{CursorKind, Modifier, Rect, Style},
23 graphics::{CursorKind, Modifier, Rect, Style},
23 info::Info,
24 info::Info,
24 input::KeyEvent,
25 input::KeyEvent,
@@ -80,7 +81,7 @@ impl EditorView {
80 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
81 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
81 Box::new(syntax::merge(
82 Box::new(syntax::merge(
82 highlights,
83 highlights,
83− Self::doc_selection_highlights(doc, view, theme),
84+ Self::doc_selection_highlights(doc, view, theme, &config.cursor_shape),
84 ))
85 ))
85 } else {
86 } else {
86 Box::new(highlights)
87 Box::new(highlights)
@@ -195,11 +196,16 @@ impl EditorView {
195 doc: &Document,
196 doc: &Document,
196 view: &View,
197 view: &View,
197 theme: &Theme,
198 theme: &Theme,
199+ cursor_shape_config: &CursorShapeConfig,
198 ) -> Vec<(usize, std::ops::Range<usize>)> {
200 ) -> Vec<(usize, std::ops::Range<usize>)> {
199 let text = doc.text().slice(..);
201 let text = doc.text().slice(..);
200 let selection = doc.selection(view.id);
202 let selection = doc.selection(view.id);
201 let primary_idx = selection.primary_index();
203 let primary_idx = selection.primary_index();
202
204
205+ let mode = doc.mode();
206+ let cursorkind = cursor_shape_config.from_mode(mode);
207+ let cursor_is_block = cursorkind == CursorKind::Block;
208+
203 let selection_scope = theme
209 let selection_scope = theme
204 .find_scope_index("ui.selection")
210 .find_scope_index("ui.selection")
205 .expect("could not find `ui.selection` scope in the theme!");
211 .expect("could not find `ui.selection` scope in the theme!");
@@ -207,7 +213,7 @@ impl EditorView {
207 .find_scope_index("ui.cursor")
213 .find_scope_index("ui.cursor")
208 .unwrap_or(selection_scope);
214 .unwrap_or(selection_scope);
209
215
210− let cursor_scope = match doc.mode() {
216+ let cursor_scope = match mode {
211 Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
217 Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
212 Mode::Select => theme.find_scope_index("ui.cursor.select"),
218 Mode::Select => theme.find_scope_index("ui.cursor.select"),
213 Mode::Normal => Some(base_cursor_scope),
219 Mode::Normal => Some(base_cursor_scope),
@@ -223,7 +229,8 @@ impl EditorView {
223
229
224 let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
230 let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
225 for (i, range) in selection.iter().enumerate() {
231 for (i, range) in selection.iter().enumerate() {
232+ let selection_is_primary = i == primary_idx;
226− let (cursor_scope, selection_scope) = if i == primary_idx {
233+ let (cursor_scope, selection_scope) = if selection_is_primary {
227 (primary_cursor_scope, primary_selection_scope)
234 (primary_cursor_scope, primary_selection_scope)
228 } else {
235 } else {
229 (cursor_scope, selection_scope)
236 (cursor_scope, selection_scope)
@@ -231,7 +238,14 @@ impl EditorView {
231
238
232 // Special-case: cursor at end of the rope.
239 // Special-case: cursor at end of the rope.
233 if range.head == range.anchor && range.head == text.len_chars() {
240 if range.head == range.anchor && range.head == text.len_chars() {
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.
234− spans.push((cursor_scope, range.head..range.head + 1));
247+ spans.push((cursor_scope, range.head..range.head + 1));
248+ }
235 continue;
249 continue;
236 }
250 }
237
251
@@ -240,11 +254,15 @@ impl EditorView {
240 // Standard case.
254 // Standard case.
241 let cursor_start = prev_grapheme_boundary(text, range.head);
255 let cursor_start = prev_grapheme_boundary(text, range.head);
242 spans.push((selection_scope, range.anchor..cursor_start));
256 spans.push((selection_scope, range.anchor..cursor_start));
257+ if !selection_is_primary || cursor_is_block {
243− spans.push((cursor_scope, cursor_start..range.head));
258+ spans.push((cursor_scope, cursor_start..range.head));
259+ }
244 } else {
260 } else {
245 // Reverse case.
261 // Reverse case.
246 let cursor_end = next_grapheme_boundary(text, range.head);
262 let cursor_end = next_grapheme_boundary(text, range.head);
263+ if !selection_is_primary || cursor_is_block {
247− spans.push((cursor_scope, range.head..cursor_end));
264+ spans.push((cursor_scope, range.head..cursor_end));
265+ }
248 spans.push((selection_scope, cursor_end..range.anchor));
266 spans.push((selection_scope, cursor_end..range.anchor));
249 }
267 }
250 }
268 }
@@ -1141,11 +1159,11 @@ impl Component for EditorView {
1141 }
1159 }
1142
1160
1143 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1161 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1144− // match view.doc.mode() {
1162+ match editor.cursor() {
1145− // Mode::Insert => write!(stdout, "\x1B[6 q"),
1163+ // All block cursors are drawn manually
1146− // mode => write!(stdout, "\x1B[2 q"),
1164+ (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
1165+ cursor => cursor,
1147− // };
1166+ }
1148− editor.cursor()
1149 }
1167 }
1150}
1168}
1151
1169
helix-view/src/document.rs+13−3
@@ -1,5 +1,6 @@
1use anyhow::{anyhow, Context, Error};
1use anyhow::{anyhow, Context, Error};
2use serde::de::{self, Deserialize, Deserializer};
2use serde::de::{self, Deserialize, Deserializer};
3+use serde::Serialize;
3use std::cell::Cell;
4use std::cell::Cell;
4use std::collections::HashMap;
5use std::collections::HashMap;
5use std::fmt::Display;
6use std::fmt::Display;
@@ -30,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
30
31
31#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
32#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
32pub enum Mode {
33pub enum Mode {
33− Normal,
34+ Normal = 0,
34− Select,
35+ Select = 1,
35− Insert,
36+ Insert = 2,
36}
37}
37
38
38impl Display for Mode {
39impl Display for Mode {
@@ -69,6 +70,15 @@ impl<'de> Deserialize<'de> for Mode {
69 }
70 }
70}
71}
71
72
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+
72pub struct Document {
82pub struct Document {
73 pub(crate) id: DocumentId,
83 pub(crate) id: DocumentId,
74 text: Rope,
84 text: Rope,
helix-view/src/editor.rs+63−6
@@ -1,6 +1,6 @@
1use crate::{
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3− document::SCRATCH_BUFFER_NAME,
3+ document::{Mode, SCRATCH_BUFFER_NAME},
4 graphics::{CursorKind, Rect},
4 graphics::{CursorKind, Rect},
5 input::KeyEvent,
5 input::KeyEvent,
6 theme::{self, Theme},
6 theme::{self, Theme},
@@ -10,7 +10,7 @@ use crate::{
10
10
11use futures_util::future;
11use futures_util::future;
12use std::{
12use std::{
13− collections::BTreeMap,
13+ collections::{BTreeMap, HashMap},
14 io::stdin,
14 io::stdin,
15 num::NonZeroUsize,
15 num::NonZeroUsize,
16 path::{Path, PathBuf},
16 path::{Path, PathBuf},
@@ -27,7 +27,7 @@ pub use helix_core::register::Registers;
27use helix_core::syntax;
27use helix_core::syntax;
28use helix_core::{Position, Selection};
28use helix_core::{Position, Selection};
29
29
30−use serde::{Deserialize, Serialize};
30+use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
31
31
32fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
32fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
33where
33where
@@ -105,16 +105,71 @@ pub struct Config {
105 /// Whether to display infoboxes. Defaults to true.
105 /// Whether to display infoboxes. Defaults to true.
106 pub auto_info: bool,
106 pub auto_info: bool,
107 pub file_picker: FilePickerConfig,
107 pub file_picker: FilePickerConfig,
108+ /// Shape for cursor in each mode
109+ pub cursor_shape: CursorShapeConfig,
108 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
110 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
109 pub true_color: bool,
111 pub true_color: bool,
110}
112}
111
113
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+
112#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "kebab-case")]
169#[serde(rename_all = "kebab-case")]
114pub enum LineNumber {
170pub enum LineNumber {
115 /// Show absolute line number
171 /// Show absolute line number
116 Absolute,
172 Absolute,
117−
118 /// Show relative line number to the primary cursor
173 /// Show relative line number to the primary cursor
119 Relative,
174 Relative,
120}
175}
@@ -151,6 +206,7 @@ impl Default for Config {
151 completion_trigger_len: 2,
206 completion_trigger_len: 2,
152 auto_info: true,
207 auto_info: true,
153 file_picker: FilePickerConfig::default(),
208 file_picker: FilePickerConfig::default(),
209+ cursor_shape: CursorShapeConfig::default(),
154 true_color: false,
210 true_color: false,
155 }
211 }
156 }
212 }
@@ -614,9 +670,10 @@ impl Editor {
614 let inner = view.inner_area();
670 let inner = view.inner_area();
615 pos.col += inner.x as usize;
671 pos.col += inner.x as usize;
616 pos.row += inner.y as usize;
672 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)
618 } else {
675 } else {
619− (None, CursorKind::Hidden)
676+ (None, CursorKind::default())
620 }
677 }
621 }
678 }
622
679
helix-view/src/graphics.rs+9−1
@@ -1,10 +1,12 @@
1use bitflags::bitflags;
1use bitflags::bitflags;
2+use serde::{Deserialize, Serialize};
2use std::{
3use std::{
3 cmp::{max, min},
4 cmp::{max, min},
4 str::FromStr,
5 str::FromStr,
5};
6};
6
7
7−#[derive(Debug, Clone, Copy, PartialEq)]
8+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
9+#[serde(rename_all = "lowercase")]
8/// UNSTABLE
10/// UNSTABLE
9pub enum CursorKind {
11pub enum CursorKind {
10 /// █
12 /// █
@@ -17,6 +19,12 @@ pub enum CursorKind {
17 Hidden,
19 Hidden,
18}
20}
19
21
22+impl Default for CursorKind {
23+ fn default() -> Self {
24+ Self::Block
25+ }
26+}
27+
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct Margin {
29pub struct Margin {
22 pub vertical: u16,
30 pub vertical: u16,