Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ Enter after a code fence creates its closing fence. `:table` adds/removes
rows or columns and sets alignment; `:formatting` opens formatting actions.
These edits participate in undo. `:format` still formats the whole note.
`vim.wrapped_line_motions = "display"` makes `$`, `I` and `A` use the
visible wrapped row; `"logical"` uses the source line.
visible wrapped row (press `$` again at the end of a row to reach the end of
the line); `"logical"` uses the source line.

`Space l e` opens the note in `$VISUAL` or `$EDITOR` at the cursor line and
reloads it afterwards. In Vim mode, `Ctrl+Z` suspends to the shell.
Expand Down
16 changes: 16 additions & 0 deletions internal/tui/authoring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,19 @@ func TestHelpersRespectLongCodeFences(t *testing.T) {
t.Fatal("closing fence left helper disabled")
}
}

func TestTypedBacktickFenceClosesOnEnter(t *testing.T) {
a, buf := newTagTestApp(t, vimPrefs(), "# Draft")
feed(a, vim.R('o'))
typeKeys(a, "```go")
if got := buf.ed.Line(1); got != "```go" {
t.Fatalf("typed fence %q", got)
}
feed(a, vim.KeyEnter)
if got := buf.ed.Text(); got != "# Draft\n```go\n\n```" {
t.Fatalf("Enter after the fence: %q", got)
}
if got := buf.ed.Cursor(); got != (vim.Pos{Line: 2, Col: 0}) {
t.Fatalf("cursor should sit inside the block: %v", got)
}
}
1 change: 1 addition & 0 deletions internal/tui/helpdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func helpLines(a *App, filter string) []string {
"Ctrl+D Ctrl+U Ctrl+F Ctrl+B Ctrl+E Ctrl+Y zt zz zb\tScrolling",
"Ctrl+O Ctrl+I\tJump list, across notes too",
"gj gk\tMove by display row when a line wraps",
"$ $\tOn a wrapped line, $ ends the visible row and a second $ reaches the line end",
"",
"## Editor: operators and text objects",
"d c y > < gu gU g~ = J gq\tOperators; double them for a line (dd, cc, yy, >>)",
Expand Down
45 changes: 45 additions & 0 deletions internal/vim/display_motion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,48 @@ func TestDisplayRowBoundariesRespectCountsAndLogicalMode(t *testing.T) {
t.Fatal(ed.Cursor())
}
}

func TestSecondDollarOnAWrappedRowReachesTheLineEnd(t *testing.T) {
opts := DefaultOptions()
opts.WrappedLineMotions = true
rows := Hooks{DisplayRowBounds: func(p Pos) (int, int) {
if p.Col < 5 {
return 0, 5
}
if p.Col < 10 {
return 5, 10
}
return 10, 14
}}
ed := New("abcdefghijklmn", opts, rows)
ed.HandleKey(R('$'))
if ed.Cursor().Col != 4 {
t.Fatalf("first $ should stop at the row end: %v", ed.Cursor())
}
ed.HandleKey(R('$'))
if ed.Cursor().Col != 13 {
t.Fatalf("second $ should reach the line end: %v", ed.Cursor())
}
ed.HandleKey(R('$'))
if ed.Cursor().Col != 13 {
t.Fatalf("$ at the line end should stay: %v", ed.Cursor())
}
// g$ is the row motion, so it never runs on past the row.
ed.SetCursor(Pos{0, 4})
ed.Feed("g$")
if ed.Cursor().Col != 4 {
t.Fatalf("g$ at the row end left the row: %v", ed.Cursor())
}
// Operators keep the visible row, even from its last character.
ed.SetCursor(Pos{0, 4})
ed.Feed("d$")
if ed.Text() != "abcdfghijklmn" {
t.Fatalf("d$ at the row end: %q", ed.Text())
}
// Visual mode extends to the line end the same way.
ed = New("abcdefghijklmn", opts, rows)
ed.Feed("v$$")
if ed.Cursor().Col != 13 || ed.Mode() != ModeVisual {
t.Fatalf("v$$: %v %v", ed.Cursor(), ed.Mode())
}
}
3 changes: 3 additions & 0 deletions internal/vim/editor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ func TestInsertMode(t *testing.T) {
{"autopair skip", "", Pos{0, 0}, "i(a)<Esc>", "(a)", Pos{0, 2}, ModeNormal, nil},
{"autopair backspace", "", Pos{0, 0}, "i(<BS><Esc>", "", Pos{0, 0}, ModeNormal, nil},
{"no autopair before word", "x", Pos{0, 0}, "i(<Esc>", "(x", Pos{0, 0}, ModeNormal, nil},
{"autopair backtick span", "", Pos{0, 0}, "i`a`<Esc>", "`a`", Pos{0, 2}, ModeNormal, nil},
{"backtick fence stays three", "", Pos{0, 0}, "i```<Esc>", "```", Pos{0, 2}, ModeNormal, nil},
{"backtick fence with language", "", Pos{0, 0}, "i```go<Esc>", "```go", Pos{0, 4}, ModeNormal, nil},
{"insert escape jk", "", Pos{0, 0}, "iajk", "a", Pos{0, 0}, ModeNormal, func(o *Options) { o.InsertEscape = "jk" }},
{"insert escape j alone", "", Pos{0, 0}, "iaj<Esc>", "aj", Pos{0, 1}, ModeNormal, func(o *Options) { o.InsertEscape = "jk" }},
{"text replacement", "", Pos{0, 0}, "ia->b<Esc>", "a→b", Pos{0, 2}, ModeNormal, func(o *Options) { o.TextReplacementsEnabled = true }},
Expand Down
7 changes: 6 additions & 1 deletion internal/vim/insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,14 @@ func (e *Editor) insertPrintable(r rune) {
e.cursor.Col++
return
}
// A backtick after a backtick grows a run (a ``` fence, a `` span)
// instead of opening a pair: pairing every one turned ``` into ```` with
// the cursor before the fourth, so Enter split the fence instead of
// closing it.
afterBacktick := e.cursor.Col > 0 && line[e.cursor.Col-1] == '`'
e.insertRaw(string(r))
if closer, ok := autoPairOpeners[r]; ok && e.opts.AutoPairs && (next == 0 || isBlank(next) || next == ')' || next == ']' || next == '}') {
if r != '`' || next == 0 || isBlank(next) {
if r != '`' || ((next == 0 || isBlank(next)) && !afterBacktick) {
e.buf.InsertText(e.cursor, string(closer))
e.ins.pairedInsert = e.cursor
}
Expand Down
16 changes: 15 additions & 1 deletion internal/vim/motions.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,28 @@ func parseMotion(keys []Key) (motion, parseStatus) {
return motion{}, parseInvalid
}

// atWrappedRowEnd reports a cursor already on the last character of a
// soft-wrapped row that is not the line's last. A bare `$` stops at the end
// of the visible row, so pressing it again there goes on to the end of the
// line, where Vim's `$` lands; otherwise the second press did nothing and
// the true end was out of reach. Operators keep the row (`d$` deletes what
// the user can see).
func (e *Editor) atWrappedRowEnd(from Pos, forOperator bool) bool {
if forOperator {
return false
}
start, end := e.hooks.DisplayRowBounds(from)
return end < e.buf.LineLen(from.Line) && from.Col >= max(start, end-1)
}

// applyMotion computes where a motion lands from `from`. forOperator marks
// operator-pending use, where a few motions behave differently (cw, e).
func (e *Editor) applyMotion(m motion, count int, hasCount bool, from Pos, forOperator bool) (motionResult, bool) {
n := max(1, count)
line := e.buf.Line(from.Line)
lineCount := e.buf.LineCount()
res := motionResult{pos: from}
if e.hooks.DisplayRowBounds != nil && ((m.kind == "$" && n == 1 && e.opts.WrappedLineMotions) || m.kind == "g$" || m.kind == "g0" || m.kind == "g^") {
if e.hooks.DisplayRowBounds != nil && ((m.kind == "$" && n == 1 && e.opts.WrappedLineMotions && !e.atWrappedRowEnd(from, forOperator)) || m.kind == "g$" || m.kind == "g0" || m.kind == "g^") {
start, end := e.hooks.DisplayRowBounds(from)
res.pos.Col = start
if m.kind == "$" || m.kind == "g$" {
Expand Down
Loading