mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
vim: Fix relative motions (#2888)
This changes vim motions to be relative to fold lines, not display lines, to match the behaviour of vim. This is necessary for relative line numbers to make sense (as the most important thing is you can do `3j` to get th e line that is numbered 3). Release Notes: - vim: Fix handling of motions when `soft_wrap` is enabled in zed. Like in vim `j,k,up,down,$,^,0,home,end` will all now navigate in file coordinates not display coordinates. - vim: Add `g {j,k,up,down,$,^,0,home,end}` to navigate in display coordinates. - vim: Add `z o` and `z c` to open and close folds. - vim: Add `z f` in visual mode to fold selection. Note: this may be a jarring change if you're grown used to the current behaviour of `j` and `k`. You can make the issue less acute by setting `"soft_wrap":"none"` in your settings; or you can manually copy the bindings for `g j` to the binding for `j` (etc.) in your keymap.json to preserve the existing behaviour.
This commit is contained in:
commit
dd577074f2
@ -137,10 +137,67 @@
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"g j": [
|
||||
"vim::Down",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g down": [
|
||||
"vim::Down",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g k": [
|
||||
"vim::Up",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g up": [
|
||||
"vim::Up",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g $": [
|
||||
"vim::EndOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g end": [
|
||||
"vim::EndOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g 0": [
|
||||
"vim::StartOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g home": [
|
||||
"vim::StartOfLine",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g ^": [
|
||||
"vim::FirstNonWhitespace",
|
||||
{
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
// z commands
|
||||
"z t": "editor::ScrollCursorTop",
|
||||
"z z": "editor::ScrollCursorCenter",
|
||||
"z b": "editor::ScrollCursorBottom",
|
||||
"z c": "editor::Fold",
|
||||
"z o": "editor::UnfoldLines",
|
||||
"z f": "editor::FoldSelectedRanges",
|
||||
// Count support
|
||||
"1": [
|
||||
"vim::Number",
|
||||
|
@ -30,6 +30,7 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
pub use self::fold_map::FoldPoint;
|
||||
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@ -310,7 +311,7 @@ impl DisplayMap {
|
||||
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
fold_snapshot: fold_map::FoldSnapshot,
|
||||
pub fold_snapshot: fold_map::FoldSnapshot,
|
||||
inlay_snapshot: inlay_map::InlaySnapshot,
|
||||
tab_snapshot: tab_map::TabSnapshot,
|
||||
wrap_snapshot: wrap_map::WrapSnapshot,
|
||||
@ -438,6 +439,20 @@ impl DisplaySnapshot {
|
||||
fold_point.to_inlay_point(&self.fold_snapshot)
|
||||
}
|
||||
|
||||
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
self.tab_snapshot.to_fold_point(tab_point, bias).0
|
||||
}
|
||||
|
||||
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> DisplayPoint {
|
||||
DisplayPoint(self.block_snapshot.max_point())
|
||||
}
|
||||
|
@ -7346,7 +7346,7 @@ impl Editor {
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
for selection in selections {
|
||||
let range = selection.range().sorted();
|
||||
let buffer_start_row = range.start.row;
|
||||
@ -7422,7 +7422,17 @@ impl Editor {
|
||||
|
||||
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let ranges = selections.into_iter().map(|s| s.start..s.end);
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let line_mode = self.selections.line_mode;
|
||||
let ranges = selections.into_iter().map(|s| {
|
||||
if line_mode {
|
||||
let start = Point::new(s.start.row, 0);
|
||||
let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row));
|
||||
start..end
|
||||
} else {
|
||||
s.start..s.end
|
||||
}
|
||||
});
|
||||
self.fold_ranges(ranges, true, cx);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use std::{cmp, sync::Arc};
|
||||
|
||||
use editor::{
|
||||
char_kind,
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
|
||||
movement, Bias, CharKind, DisplayPoint, ToOffset,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
||||
@ -21,16 +21,16 @@ use crate::{
|
||||
pub enum Motion {
|
||||
Left,
|
||||
Backspace,
|
||||
Down,
|
||||
Up,
|
||||
Down { display_lines: bool },
|
||||
Up { display_lines: bool },
|
||||
Right,
|
||||
NextWordStart { ignore_punctuation: bool },
|
||||
NextWordEnd { ignore_punctuation: bool },
|
||||
PreviousWordStart { ignore_punctuation: bool },
|
||||
FirstNonWhitespace,
|
||||
FirstNonWhitespace { display_lines: bool },
|
||||
CurrentLine,
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
StartOfLine { display_lines: bool },
|
||||
EndOfLine { display_lines: bool },
|
||||
StartOfParagraph,
|
||||
EndOfParagraph,
|
||||
StartOfDocument,
|
||||
@ -62,6 +62,41 @@ struct PreviousWordStart {
|
||||
ignore_punctuation: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Up {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Down {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FirstNonWhitespace {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EndOfLine {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StartOfLine {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
struct RepeatFind {
|
||||
#[serde(default)]
|
||||
@ -73,12 +108,7 @@ actions!(
|
||||
[
|
||||
Left,
|
||||
Backspace,
|
||||
Down,
|
||||
Up,
|
||||
Right,
|
||||
FirstNonWhitespace,
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
CurrentLine,
|
||||
StartOfParagraph,
|
||||
EndOfParagraph,
|
||||
@ -90,20 +120,63 @@ actions!(
|
||||
);
|
||||
impl_actions!(
|
||||
vim,
|
||||
[NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
|
||||
[
|
||||
NextWordStart,
|
||||
NextWordEnd,
|
||||
PreviousWordStart,
|
||||
RepeatFind,
|
||||
Up,
|
||||
Down,
|
||||
FirstNonWhitespace,
|
||||
EndOfLine,
|
||||
StartOfLine,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
|
||||
motion(Motion::FirstNonWhitespace, cx)
|
||||
cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
|
||||
motion(
|
||||
Motion::Down {
|
||||
display_lines: action.display_lines,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
|
||||
motion(
|
||||
Motion::Up {
|
||||
display_lines: action.display_lines,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
||||
cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
|
||||
motion(
|
||||
Motion::FirstNonWhitespace {
|
||||
display_lines: action.display_lines,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
|
||||
motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: action.display_lines,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
|
||||
motion(
|
||||
Motion::EndOfLine {
|
||||
display_lines: action.display_lines,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
|
||||
motion(Motion::StartOfParagraph, cx)
|
||||
@ -192,19 +265,25 @@ impl Motion {
|
||||
pub fn linewise(&self) -> bool {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
|
||||
| StartOfParagraph | EndOfParagraph => true,
|
||||
EndOfLine
|
||||
Down { .. }
|
||||
| Up { .. }
|
||||
| StartOfDocument
|
||||
| EndOfDocument
|
||||
| CurrentLine
|
||||
| NextLineStart
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph => true,
|
||||
EndOfLine { .. }
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
| Left
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| StartOfLine { .. }
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
| FirstNonWhitespace { .. }
|
||||
| FindBackward { .. } => false,
|
||||
}
|
||||
}
|
||||
@ -213,21 +292,21 @@ impl Motion {
|
||||
use Motion::*;
|
||||
match self {
|
||||
StartOfDocument | EndOfDocument | CurrentLine => true,
|
||||
Down
|
||||
| Up
|
||||
| EndOfLine
|
||||
Down { .. }
|
||||
| Up { .. }
|
||||
| EndOfLine { .. }
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
| Left
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| StartOfLine { .. }
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
| FirstNonWhitespace { .. }
|
||||
| FindBackward { .. }
|
||||
| NextLineStart => false,
|
||||
}
|
||||
@ -236,12 +315,12 @@ impl Motion {
|
||||
pub fn inclusive(&self) -> bool {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Down
|
||||
| Up
|
||||
Down { .. }
|
||||
| Up { .. }
|
||||
| StartOfDocument
|
||||
| EndOfDocument
|
||||
| CurrentLine
|
||||
| EndOfLine
|
||||
| EndOfLine { .. }
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
@ -249,12 +328,12 @@ impl Motion {
|
||||
Left
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| StartOfLine { .. }
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
| FirstNonWhitespace { .. }
|
||||
| FindBackward { .. } => false,
|
||||
}
|
||||
}
|
||||
@ -272,8 +351,18 @@ impl Motion {
|
||||
let (new_point, goal) = match self {
|
||||
Left => (left(map, point, times), SelectionGoal::None),
|
||||
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
||||
Down => down(map, point, goal, times),
|
||||
Up => up(map, point, goal, times),
|
||||
Down {
|
||||
display_lines: false,
|
||||
} => down(map, point, goal, times),
|
||||
Down {
|
||||
display_lines: true,
|
||||
} => down_display(map, point, goal, times),
|
||||
Up {
|
||||
display_lines: false,
|
||||
} => up(map, point, goal, times),
|
||||
Up {
|
||||
display_lines: true,
|
||||
} => up_display(map, point, goal, times),
|
||||
Right => (right(map, point, times), SelectionGoal::None),
|
||||
NextWordStart { ignore_punctuation } => (
|
||||
next_word_start(map, point, *ignore_punctuation, times),
|
||||
@ -287,9 +376,17 @@ impl Motion {
|
||||
previous_word_start(map, point, *ignore_punctuation, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
|
||||
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
|
||||
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
FirstNonWhitespace { display_lines } => (
|
||||
first_non_whitespace(map, *display_lines, point),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
StartOfLine { display_lines } => (
|
||||
start_of_line(map, *display_lines, point),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
EndOfLine { display_lines } => {
|
||||
(end_of_line(map, *display_lines, point), SelectionGoal::None)
|
||||
}
|
||||
StartOfParagraph => (
|
||||
movement::start_of_paragraph(map, point, times),
|
||||
SelectionGoal::None,
|
||||
@ -298,7 +395,7 @@ impl Motion {
|
||||
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
|
||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (
|
||||
end_of_document(map, point, maybe_times),
|
||||
@ -399,6 +496,33 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
|
||||
}
|
||||
|
||||
fn down(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
|
||||
let goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => {
|
||||
goal = SelectionGoal::Column(start.column());
|
||||
start.column()
|
||||
}
|
||||
};
|
||||
|
||||
let new_row = cmp::min(
|
||||
start.row() + times as u32,
|
||||
map.buffer_snapshot.max_point().row,
|
||||
);
|
||||
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
|
||||
let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
|
||||
|
||||
(map.clip_point(point, Bias::Left), goal)
|
||||
}
|
||||
|
||||
fn down_display(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
@ -407,10 +531,35 @@ fn down(
|
||||
for _ in 0..times {
|
||||
(point, goal) = movement::down(map, point, goal, true);
|
||||
}
|
||||
|
||||
(point, goal)
|
||||
}
|
||||
|
||||
fn up(
|
||||
pub(crate) fn up(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
|
||||
let goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => {
|
||||
goal = SelectionGoal::Column(start.column());
|
||||
start.column()
|
||||
}
|
||||
};
|
||||
|
||||
let new_row = start.row().saturating_sub(times as u32);
|
||||
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
|
||||
let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
|
||||
|
||||
(map.clip_point(point, Bias::Left), goal)
|
||||
}
|
||||
|
||||
fn up_display(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
@ -419,6 +568,7 @@ fn up(
|
||||
for _ in 0..times {
|
||||
(point, goal) = movement::up(map, point, goal, true);
|
||||
}
|
||||
|
||||
(point, goal)
|
||||
}
|
||||
|
||||
@ -509,8 +659,12 @@ fn previous_word_start(
|
||||
point
|
||||
}
|
||||
|
||||
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
|
||||
let mut last_point = DisplayPoint::new(from.row(), 0);
|
||||
fn first_non_whitespace(
|
||||
map: &DisplaySnapshot,
|
||||
display_lines: bool,
|
||||
from: DisplayPoint,
|
||||
) -> DisplayPoint {
|
||||
let mut last_point = start_of_line(map, display_lines, from);
|
||||
let language = map.buffer_snapshot.language_at(from.to_point(map));
|
||||
for (ch, point) in map.chars_at(last_point) {
|
||||
if ch == '\n' {
|
||||
@ -527,12 +681,31 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi
|
||||
map.clip_point(last_point, Bias::Left)
|
||||
}
|
||||
|
||||
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
pub(crate) fn start_of_line(
|
||||
map: &DisplaySnapshot,
|
||||
display_lines: bool,
|
||||
point: DisplayPoint,
|
||||
) -> DisplayPoint {
|
||||
if display_lines {
|
||||
map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
|
||||
} else {
|
||||
map.prev_line_boundary(point.to_point(map)).1
|
||||
}
|
||||
}
|
||||
|
||||
fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
pub(crate) fn end_of_line(
|
||||
map: &DisplaySnapshot,
|
||||
display_lines: bool,
|
||||
point: DisplayPoint,
|
||||
) -> DisplayPoint {
|
||||
if display_lines {
|
||||
map.clip_point(
|
||||
DisplayPoint::new(point.row(), map.line_len(point.row())),
|
||||
Bias::Left,
|
||||
)
|
||||
} else {
|
||||
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
|
||||
}
|
||||
}
|
||||
|
||||
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
|
||||
@ -654,11 +827,8 @@ fn find_backward(
|
||||
}
|
||||
|
||||
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
|
||||
first_non_whitespace(
|
||||
map,
|
||||
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left),
|
||||
)
|
||||
let correct_line = down(map, point, SelectionGoal::None, times).0;
|
||||
first_non_whitespace(map, false, correct_line)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -10,7 +10,7 @@ mod yank;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
motion::{self, Motion},
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
Vim,
|
||||
@ -78,13 +78,27 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let times = vim.pop_number_operator(cx);
|
||||
change_motion(vim, Motion::EndOfLine, times, cx);
|
||||
change_motion(
|
||||
vim,
|
||||
Motion::EndOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
times,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::EndOfLine, times, cx);
|
||||
delete_motion(
|
||||
vim,
|
||||
Motion::EndOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
times,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
scroll::init(cx);
|
||||
@ -165,7 +179,10 @@ fn insert_first_non_whitespace(
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
|
||||
Motion::FirstNonWhitespace {
|
||||
display_lines: false,
|
||||
}
|
||||
.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -178,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, None)
|
||||
Motion::CurrentLine.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -197,8 +214,8 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
.collect();
|
||||
let edits = selection_start_rows.into_iter().map(|row| {
|
||||
let (indent, _) = map.line_indent(row);
|
||||
let start_of_line = map
|
||||
.clip_point(DisplayPoint::new(row, 0), Bias::Left)
|
||||
let start_of_line =
|
||||
motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
|
||||
.to_point(&map);
|
||||
let mut new_text = " ".repeat(indent as usize);
|
||||
new_text.push('\n');
|
||||
@ -206,10 +223,10 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.row_mut() -= 1;
|
||||
*cursor.column_mut() = map.line_len(cursor.row());
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
|
||||
let insert_point = motion::end_of_line(map, false, previous_line);
|
||||
(insert_point, SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -223,22 +240,23 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (map, old_selections) = editor.selections.all_display(cx);
|
||||
|
||||
let selection_end_rows: HashSet<u32> = old_selections
|
||||
.into_iter()
|
||||
.map(|selection| selection.end.row())
|
||||
.collect();
|
||||
let edits = selection_end_rows.into_iter().map(|row| {
|
||||
let (indent, _) = map.line_indent(row);
|
||||
let end_of_line = map
|
||||
.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
|
||||
.to_point(&map);
|
||||
let end_of_line =
|
||||
motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
|
||||
|
||||
let mut new_text = "\n".to_string();
|
||||
new_text.push_str(&" ".repeat(indent as usize));
|
||||
(end_of_line..end_of_line, new_text)
|
||||
});
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, None)
|
||||
Motion::CurrentLine.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
|
@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
// Some motions ignore failure when switching to normal mode
|
||||
let mut motion_succeeded = matches!(
|
||||
motion,
|
||||
Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
|
||||
Motion::Left
|
||||
| Motion::Right
|
||||
| Motion::EndOfLine { .. }
|
||||
| Motion::Backspace
|
||||
| Motion::StartOfLine { .. }
|
||||
);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
|
@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
|
||||
}
|
||||
if line_mode {
|
||||
Motion::CurrentLine.expand_selection(map, selection, None, false);
|
||||
if let Some((point, _)) = Motion::FirstNonWhitespace.move_point(
|
||||
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
||||
display_lines: false,
|
||||
})
|
||||
.move_point(
|
||||
map,
|
||||
selection.start,
|
||||
selection.goal,
|
||||
|
@ -285,3 +285,218 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
|
||||
Mode::Visual,
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_wrap(12).await;
|
||||
// tests line wrap as follows:
|
||||
// 1: twelve char
|
||||
// twelve char
|
||||
// 2: twelve char
|
||||
cx.set_shared_state(indoc! { "
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve char
|
||||
tˇwelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["k"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["g", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char tˇwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["g", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve char
|
||||
tˇwelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "k"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char tˇwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "^"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char ˇtwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["^"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
ˇtwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["g", "$"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve charˇ twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["$"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve chaˇr
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["enter"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char twelve char
|
||||
ˇtwelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
twelve char
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["o", "o", "escape"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
twelve char twelve char
|
||||
ˇo
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
twelve char
|
||||
tˇwelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-a", "a", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
twelve char twelve charˇa
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-i", "i", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
ˇitwelve char twelve chara
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-d"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
ˇ
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
twelve char
|
||||
twelve char tˇwelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-o", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
twelve char
|
||||
ˇo
|
||||
twelve char twelve char
|
||||
twelve char
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_folds(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_neovim_option("foldmethod=manual").await;
|
||||
|
||||
cx.set_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
ˇbarp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
|
||||
.await;
|
||||
|
||||
// visual display is now:
|
||||
// fn boop () {
|
||||
// [FOLDED]
|
||||
// }
|
||||
|
||||
// TODO: this should not be needed but currently zf does not
|
||||
// return to normal mode.
|
||||
cx.simulate_shared_keystrokes(["escape"]).await;
|
||||
|
||||
// skip over fold downward
|
||||
cx.simulate_shared_keystrokes(["g", "g"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
ˇfn boop() {
|
||||
barp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["j", "j"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
barp()
|
||||
bazp()
|
||||
ˇ}
|
||||
"})
|
||||
.await;
|
||||
|
||||
// skip over fold upward
|
||||
cx.simulate_shared_keystrokes(["2", "k"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
ˇfn boop() {
|
||||
barp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
|
||||
// yank the fold
|
||||
cx.simulate_shared_keystrokes(["down", "y", "y"]).await;
|
||||
cx.assert_shared_clipboard(" barp()\n bazp()\n").await;
|
||||
|
||||
// re-open
|
||||
cx.simulate_shared_keystrokes(["z", "o"]).await;
|
||||
cx.assert_shared_state(indoc! { "
|
||||
fn boop() {
|
||||
ˇ barp()
|
||||
bazp()
|
||||
}
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
use indoc::indoc;
|
||||
use settings::SettingsStore;
|
||||
use std::ops::{Deref, DerefMut, Range};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::ContextHandle;
|
||||
use language::OffsetRangeExt;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, SoftWrap},
|
||||
OffsetRangeExt,
|
||||
};
|
||||
use util::test::{generate_marked_text, marked_text_offsets};
|
||||
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
@ -127,6 +131,27 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
context_handle
|
||||
}
|
||||
|
||||
pub async fn set_shared_wrap(&mut self, columns: u32) {
|
||||
if columns < 12 {
|
||||
panic!("nvim doesn't support columns < 12")
|
||||
}
|
||||
self.neovim.set_option("wrap").await;
|
||||
self.neovim.set_option("columns=12").await;
|
||||
|
||||
self.update(|cx| {
|
||||
cx.update_global(|settings: &mut SettingsStore, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength);
|
||||
settings.defaults.preferred_line_length = Some(columns);
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_neovim_option(&mut self, option: &str) {
|
||||
self.neovim.set_option(option).await;
|
||||
}
|
||||
|
||||
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
||||
let neovim = self.neovim_state().await;
|
||||
let editor = self.editor_state();
|
||||
|
@ -41,6 +41,7 @@ pub enum NeovimData {
|
||||
Key(String),
|
||||
Get { state: String, mode: Option<Mode> },
|
||||
ReadRegister { name: char, value: String },
|
||||
SetOption { value: String },
|
||||
}
|
||||
|
||||
pub struct NeovimConnection {
|
||||
@ -222,6 +223,29 @@ impl NeovimConnection {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_option(&mut self, value: &str) {
|
||||
self.nvim
|
||||
.command_output(format!("set {}", value).as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.data.push_back(NeovimData::SetOption {
|
||||
value: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_option(&mut self, value: &str) {
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::SetOption {
|
||||
value: value.to_string(),
|
||||
}),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn read_register(&mut self, register: char) -> String {
|
||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||
|
@ -51,8 +51,15 @@ pub fn init(cx: &mut AppContext) {
|
||||
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) {
|
||||
let is_up_or_down = matches!(motion, Motion::Up | Motion::Down);
|
||||
if vim.state().mode == Mode::VisualBlock
|
||||
&& !matches!(
|
||||
motion,
|
||||
Motion::EndOfLine {
|
||||
display_lines: false
|
||||
}
|
||||
)
|
||||
{
|
||||
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
|
||||
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
|
||||
motion.move_point(map, point, goal, times)
|
||||
})
|
||||
|
23
crates/vim/test_data/test_folds.json
Normal file
23
crates/vim/test_data/test_folds.json
Normal file
@ -0,0 +1,23 @@
|
||||
{"SetOption":{"value":"foldmethod=manual"}}
|
||||
{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"z"}
|
||||
{"Key":"f"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}}
|
||||
{"Key":"down"}
|
||||
{"Key":"y"}
|
||||
{"Key":"y"}
|
||||
{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}}
|
||||
{"Key":"z"}
|
||||
{"Key":"o"}
|
||||
{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}}
|
50
crates/vim/test_data/test_wrapped_lines.json
Normal file
50
crates/vim/test_data/test_wrapped_lines.json
Normal file
@ -0,0 +1,50 @@
|
||||
{"SetOption":{"value":"wrap"}}
|
||||
{"SetOption":{"value":"columns=12"}}
|
||||
{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}}
|
||||
{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}}
|
||||
{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}}
|
||||
{"Key":"o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}}
|
||||
{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}}
|
||||
{"Key":"shift-a"}
|
||||
{"Key":"a"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"shift-i"}
|
||||
{"Key":"i"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}}
|
||||
{"Key":"shift-d"}
|
||||
{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}}
|
||||
{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}}
|
||||
{"Key":"shift-o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}
|
Loading…
Reference in New Issue
Block a user