Skip to content

Commit

Permalink
Merge pull request #1154 from sudormrfbin/cursor-shape-new
Browse files Browse the repository at this point in the history
Change cursor shape on mode change
  • Loading branch information
archseer authored Jan 23, 2022
2 parents a8e69e1 + b3b4e78 commit e2d2f19
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 23 deletions.
43 changes: 41 additions & 2 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|--|--|---------|
Expand All @@ -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.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.
### `[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.file-picker]` 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 |
|--|--|---------|
Expand Down
40 changes: 29 additions & 11 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
editor::CursorShapeConfig,
graphics::{CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
Expand Down Expand Up @@ -80,7 +81,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)
Expand Down Expand Up @@ -195,19 +196,24 @@ 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!");
let base_cursor_scope = theme
.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),
Expand All @@ -223,15 +229,23 @@ 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)
};

// 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;
}

Expand All @@ -240,11 +254,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));
}
}
Expand Down Expand Up @@ -1141,11 +1159,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,
}
}
}

Expand Down
16 changes: 13 additions & 3 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
69 changes: 63 additions & 6 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -10,7 +10,7 @@ use crate::{

use futures_util::future;
use std::{
collections::BTreeMap,
collections::{BTreeMap, HashMap},
io::stdin,
num::NonZeroUsize,
path::{Path, PathBuf},
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -614,9 +670,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())
}
}

Expand Down
10 changes: 9 additions & 1 deletion helix-view/src/graphics.rs
Original file line number Diff line number Diff line change
@@ -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 {
/// █
Expand All @@ -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,
Expand Down

0 comments on commit e2d2f19

Please sign in to comment.