aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--book/src/configuration.md43
-rw-r--r--helix-term/src/ui/editor.rs40
-rw-r--r--helix-view/src/document.rs16
-rw-r--r--helix-view/src/editor.rs69
-rw-r--r--helix-view/src/graphics.rs10
5 files changed, 155 insertions, 23 deletions
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 33a933b2..476c2b39 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -5,9 +5,27 @@ 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]
+insert = "bar"
+normal = "block"
+select = "underline"
+
+[editor.file-picker]
+hidden = false
+```
+
## Editor
-`[editor]` section of the config.
+### `[editor]` Section
| Key | Description | Default |
|--|--|---------|
@@ -25,7 +43,28 @@ To override global configuration parameters, create a `config.toml` file located
| `auto-info` | Whether to display infoboxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
-`[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] | `block` |
+| `select` | Cursor shape in [select mode][select mode] | `block` |
+
+[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 8d811ed2..5b7e9075 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<dyn Iterator<Item = HighlightEvent>> = 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<usize>)> {
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,7 +231,7 @@ 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),
@@ -241,7 +247,8 @@ impl EditorView {
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
for (i, range) in selection.iter().enumerate() {
- let (cursor_scope, selection_scope) = if i == primary_idx {
+ let selection_is_primary = i == primary_idx;
+ let (cursor_scope, selection_scope) = if selection_is_primary {
(primary_cursor_scope, primary_selection_scope)
} else {
(cursor_scope, selection_scope)
@@ -249,7 +256,14 @@ 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 !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;
}
@@ -258,11 +272,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 !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);
- spans.push((cursor_scope, range.head..cursor_end));
+ if !selection_is_primary || cursor_is_block {
+ spans.push((cursor_scope, range.head..cursor_end));
+ }
spans.push((selection_scope, cursor_end..range.anchor));
}
}
@@ -1158,11 +1176,11 @@ impl Component for EditorView {
}
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, 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/document.rs b/helix-view/src/document.rs
index 9652d7b3..9185e483 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,5 +1,6 @@
use anyhow::{anyhow, Context, Error};
use serde::de::{self, Deserialize, Deserializer};
+use serde::Serialize;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display;
@@ -30,9 +31,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 {
@@ -69,6 +70,15 @@ impl<'de> Deserialize<'de> for Mode {
}
}
+impl Serialize for Mode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index f4b0f73e..eef7520e 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},
input::KeyEvent,
theme::{self, Theme},
@@ -10,7 +10,7 @@ use crate::{
use futures_util::future;
use std::{
- collections::BTreeMap,
+ collections::{BTreeMap, HashMap},
io::stdin,
num::NonZeroUsize,
path::{Path, PathBuf},
@@ -27,7 +27,7 @@ pub use helix_core::register::Registers;
use helix_core::syntax;
use helix_core::{Position, Selection};
-use serde::{Deserialize, Serialize};
+use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
@@ -105,16 +105,71 @@ 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,
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
pub true_color: bool,
}
+// 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 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<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let m = HashMap::<Mode, CursorKind>::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 Serialize for CursorShapeConfig {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(self.len()))?;
+ let modes = [Mode::Normal, Mode::Select, Mode::Insert];
+ for mode in modes {
+ map.serialize_entry(&mode, &self.from_mode(mode))?;
+ }
+ map.end()
+ }
+}
+
+impl std::ops::Deref for CursorShapeConfig {
+ type Target = [CursorKind; 3];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Default for CursorShapeConfig {
+ fn default() -> Self {
+ Self([CursorKind::Block; 3])
+ }
+}
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
Absolute,
-
/// Show relative line number to the primary cursor
Relative,
}
@@ -151,6 +206,7 @@ impl Default for Config {
completion_trigger_len: 2,
auto_info: true,
file_picker: FilePickerConfig::default(),
+ cursor_shape: CursorShapeConfig::default(),
true_color: false,
}
}
@@ -623,9 +679,10 @@ 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.from_mode(doc.mode());
+ (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 b8e43ba5..d75cde82 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -1,10 +1,12 @@
use bitflags::bitflags;
+use serde::{Deserialize, Serialize};
use std::{
cmp::{max, min},
str::FromStr,
};
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
/// UNSTABLE
pub enum CursorKind {
/// █
@@ -17,6 +19,12 @@ pub enum CursorKind {
Hidden,
}
+impl Default for CursorKind {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,