Skip to content

Commit

Permalink
Add visual line mode
Browse files Browse the repository at this point in the history
Visual line mode from Vim or linewise selection mode.

helix-editor#356
  • Loading branch information
kladd committed Jan 13, 2024
1 parent 17dd102 commit 7b7b7ab
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 62 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ target
helix-term/rustfmt.toml
result
runtime/grammars
.idea/
70 changes: 11 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,16 @@
<div align="center">
# Helix with visual line mode

<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="logo_dark.svg">
<source media="(prefers-color-scheme: light)" srcset="logo_light.svg">
<img alt="Helix" height="128" src="logo_light.svg">
</picture>
</h1>
This is Helix with a simple visual line mode.

[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
[![GitHub Release](https://img.shields.io/github/v/release/helix-editor/helix)](https://github.com/helix-editor/helix/releases/latest)
[![Documentation](https://shields.io/badge/-documentation-452859)](https://docs.helix-editor.com/)
[![GitHub contributors](https://img.shields.io/github/contributors/helix-editor/helix)](https://github.com/helix-editor/helix/graphs/contributors)
[![Matrix Space](https://img.shields.io/matrix/helix-community:matrix.org)](https://matrix.to/#/#helix-community:matrix.org)
> ...
> Just like kakoune we have no plan to add a linewise selection mode.
>
> [helix-editor/helix#356](https://github.com/helix-editor/helix/issues/356#issuecomment-1785792949)
</div>
See also
* [helix-editor/helix#2317](https://github.com/helix-editor/helix/issues/2317)
* [helix-editor/helix#5548](https://github.com/helix-editor/helix/discussions/5548#discussioncomment-4694127)

![Screenshot](./screenshot.png)
---

A [Kakoune](https://github.com/mawww/kakoune) / [Neovim](https://github.com/neovim/neovim) inspired editor, written in Rust.

The editing model is very heavily based on Kakoune; during development I found
myself agreeing with most of Kakoune's design decisions.

For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).

All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html).

[Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting)

# Features

- Vim-like modal editing
- Multiple selections
- Built-in language server support
- Smart, incremental syntax highlighting and code editing via tree-sitter

It's a terminal-based editor first, but I'd like to explore a custom renderer
(similar to Emacs) in wgpu or skulpin.

Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.scm`.

# Installation

[Installation documentation](https://docs.helix-editor.com/install.html).

[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)

# Contributing

Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).

# Getting help

Your question might already be answered on the [FAQ](https://github.com/helix-editor/helix/wiki/FAQ).

Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).

# Credits

Thanks to [@jakenvac](https://github.com/jakenvac) for designing the logo!
[Helix README](https://github.com/helix-editor/helix/blob/master/README.md)
102 changes: 101 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ impl MappableCommand {
move_line_down, "Move down",
move_visual_line_up, "Move up",
move_visual_line_down, "Move down",
move_select_line_up, "Modify line selection up.",
move_select_line_down, "Modify line selection down.",
extend_char_left, "Extend left",
extend_char_right, "Extend right",
extend_line_up, "Extend up",
Expand Down Expand Up @@ -329,6 +331,8 @@ impl MappableCommand {
normal_mode, "Enter normal mode",
select_mode, "Enter selection extend mode",
exit_select_mode, "Exit selection mode",
select_line_mode, "Enter line selection extend mode",
exit_select_line_mode, "Exit line selection mode",
goto_definition, "Goto definition",
goto_declaration, "Goto declaration",
add_newline_above, "Add newline above",
Expand Down Expand Up @@ -684,6 +688,64 @@ fn extend_visual_line_down(cx: &mut Context) {
)
}

fn move_select_line_down(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);

let text = doc.text();
let selection = doc.selection(view.id).clone().transform(|range| {
let (start_line, end_line) = range.line_range(text.slice(..));

let start = text.line_to_char(start_line);
let end = text.line_to_char(
(end_line + 1) // newline of end_line
.min(text.len_lines()),
);

match range.direction() {
Direction::Backward if start_line == end_line => Range::new(
start,
text.line_to_char((end_line + count + 1).min(text.len_lines())),
),
Direction::Forward => Range::new(
start,
text.line_to_char((end_line + count + 1).min(text.len_lines())),
),
Direction::Backward => Range::new(end, text.line_to_char(start_line + count)),
}
});

doc.set_selection(view.id, selection);
}

fn move_select_line_up(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);

let text = doc.text();
let selection = doc.selection(view.id).clone().transform(|range| {
let (start_line, end_line) = range.line_range(text.slice(..));

let start = text.line_to_char(start_line);
let end = text.line_to_char(
(end_line + 1) // newline of end_line
.min(text.len_lines()),
);

match range.direction() {
Direction::Forward if start_line == end_line => {
Range::new(end, text.line_to_char(start_line.saturating_sub(count)))
}
Direction::Backward => {
Range::new(end, text.line_to_char(start_line.saturating_sub(count)))
}
Direction::Forward => Range::new(start, text.line_to_char(end_line)),
}
});

doc.set_selection(view.id, selection);
}

fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);

Expand Down Expand Up @@ -3348,6 +3410,43 @@ fn exit_select_mode(cx: &mut Context) {
}
}

fn select_line_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);

doc.set_selection(
view.id,
doc.selection(view.id).clone().transform(|range| {
let text = doc.text();

let (start_line, end_line) = range.line_range(text.slice(..));
let start = text.line_to_char(start_line);
let end = text.line_to_char((end_line + 1).min(text.len_lines()));

Range::new(start, end).with_direction(range.direction())
}),
);

cx.editor.mode = Mode::SelectLine;
}

fn exit_select_line_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id).clone().transform(|range| {
let text = doc.text();

let (start_line, end_line) = range.line_range(text.slice(..));
let first = text.line_to_char(start_line);
let last = text.line_to_char(end_line.min(text.len_lines()));

match range.direction() {
Direction::Forward => Range::new(last, last),
Direction::Backward => Range::new(first, first),
}
});
doc.set_selection(view.id, selection);
cx.editor.mode = Mode::Normal;
}

fn goto_first_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().first() {
Expand Down Expand Up @@ -4154,7 +4253,8 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
let count = cx.count();
let paste = match cx.editor.mode {
Mode::Insert | Mode::Select => Paste::Cursor,
Mode::Normal => Paste::Before,
// TODO(kladd): We'll see.
Mode::Normal | Mode::SelectLine => Paste::Before,
};
let (view, doc) = current!(cx.editor);
paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
Expand Down
10 changes: 10 additions & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"E" => move_next_long_word_end,

"v" => select_mode,
"V" => select_line_mode,
"G" => goto_line,
"g" => { "Goto"
"g" => goto_file_start,
Expand Down Expand Up @@ -385,9 +386,18 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"home" => goto_line_start,
"end" => goto_line_end_newline,
});
let mut select_line = normal.clone();
select_line.merge_nodes(keymap!({ "Select line mode"
"j" | "down" => move_select_line_down,
"k" | "up" => move_select_line_up,

"esc" => exit_select_line_mode,
"v" => select_mode,
}));
hashmap!(
Mode::Normal => normal,
Mode::Select => select,
Mode::Insert => insert,
Mode::SelectLine => select_line,
)
}
6 changes: 4 additions & 2 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,14 +446,16 @@ impl EditorView {

let cursor_scope = match mode {
Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
Mode::Select | Mode::SelectLine => theme.find_scope_index_exact("ui.cursor.select"),
Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
}
.unwrap_or(base_cursor_scope);

let primary_cursor_scope = match mode {
Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
Mode::Select | Mode::SelectLine => {
theme.find_scope_index_exact("ui.cursor.primary.select")
}
Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
}
.unwrap_or(base_primary_cursor_scope);
Expand Down
2 changes: 2 additions & 0 deletions helix-term/src/ui/statusline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ where
match context.editor.mode() {
Mode::Insert => &modenames.insert,
Mode::Select => &modenames.select,
Mode::SelectLine => &modenames.select_line,
Mode::Normal => &modenames.normal,
}
} else {
Expand All @@ -191,6 +192,7 @@ where
match context.editor.mode() {
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
Mode::SelectLine => Some(context.editor.theme.get("ui.statusline.select-line")),
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
}
} else {
Expand Down
3 changes: 3 additions & 0 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum Mode {
Normal = 0,
Select = 1,
Insert = 2,
SelectLine = 3,
}

impl Display for Mode {
Expand All @@ -60,6 +61,7 @@ impl Display for Mode {
Mode::Normal => f.write_str("normal"),
Mode::Select => f.write_str("select"),
Mode::Insert => f.write_str("insert"),
Mode::SelectLine => f.write_str("select-line"),
}
}
}
Expand All @@ -72,6 +74,7 @@ impl FromStr for Mode {
"normal" => Ok(Mode::Normal),
"select" => Ok(Mode::Select),
"insert" => Ok(Mode::Insert),
"select-line" => Ok(Mode::SelectLine),
_ => bail!("Invalid mode '{}'", s),
}
}
Expand Down
2 changes: 2 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ pub struct ModeConfig {
pub normal: String,
pub insert: String,
pub select: String,
pub select_line: String,
}

impl Default for ModeConfig {
Expand All @@ -458,6 +459,7 @@ impl Default for ModeConfig {
normal: String::from("NOR"),
insert: String::from("INS"),
select: String::from("SEL"),
select_line: String::from("SLN"),
}
}
}
Expand Down

0 comments on commit 7b7b7ab

Please sign in to comment.