From 19c69f9eaad7420896b5dc46b6d29f2d5de80010 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sat, 8 Jun 2024 19:14:30 +0200 Subject: [PATCH 01/17] Fix Cursor{Up,Down} after DeleteLine and CutLine After executing CutLine or DeleteLine action, the cursor is at the beginning of a line (as expected) but then moving the cursor up or down moves it to an unexpected location in the middle of the next or previous line. Fix this by updating the cursor's LastVisualX. --- internal/action/actions.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/action/actions.go b/internal/action/actions.go index 79bff07d..45c7c916 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1211,6 +1211,7 @@ func (h *BufPane) CutLine() bool { h.lastCutTime = time.Now() h.Cursor.DeleteSelection() h.Cursor.ResetSelection() + h.Cursor.StoreVisualX() InfoBar.Message("Cut line") h.Relocate() return true @@ -1256,6 +1257,7 @@ func (h *BufPane) DeleteLine() bool { } h.Cursor.DeleteSelection() h.Cursor.ResetSelection() + h.Cursor.StoreVisualX() InfoBar.Message("Deleted line") h.Relocate() return true From df8d5285bfa806b1b09734beb8adbdb5edb53a38 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 00:24:54 +0200 Subject: [PATCH 02/17] Fix Cursor{Up,Down} after CopyLine After executing the CopyLine action, moving cursor up or down unexpectedly moves cursor to the beginning of the line, since its LastVisualX value is lost in the selection/deselection manipulations. Fix this by restoring the original LastVisualX. --- internal/action/actions.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/action/actions.go b/internal/action/actions.go index 45c7c916..7b367011 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1179,6 +1179,7 @@ func (h *BufPane) CopyLine() bool { return false } origLoc := h.Cursor.Loc + origLastVisualX := h.Cursor.LastVisualX h.Cursor.SelectLine() h.Cursor.CopySelection(clipboard.ClipboardReg) h.freshClip = true @@ -1186,6 +1187,7 @@ func (h *BufPane) CopyLine() bool { h.Cursor.Deselect(true) h.Cursor.Loc = origLoc + h.Cursor.LastVisualX = origLastVisualX h.Relocate() return true } From 8bc67569f96c2503260100ce4d10bc1169e89dbe Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 01:46:46 +0200 Subject: [PATCH 03/17] Fix CopyLine at the last empty line of buffer When the cursor is at the last line of buffer and it is an empty line, CopyLine does not copy this line, which is correct, but it shows a bogus "Copied line" message. Fix this by adding a check for that, same as in CutLine and DeleteLine. --- internal/action/actions.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/action/actions.go b/internal/action/actions.go index 7b367011..5cb177ea 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1181,6 +1181,9 @@ func (h *BufPane) CopyLine() bool { origLoc := h.Cursor.Loc origLastVisualX := h.Cursor.LastVisualX h.Cursor.SelectLine() + if !h.Cursor.HasSelection() { + return false + } h.Cursor.CopySelection(clipboard.ClipboardReg) h.freshClip = true InfoBar.Message("Copied line") From 52ed4315ff521f27b9042f8dfa292cb618370f51 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 12:07:07 +0200 Subject: [PATCH 04/17] Make lastCutTime actually work The CutLine action has a feature: if we execute it multiple times to cut multiple lines, new cut lines are added to the previously cut lines in the clipboard instead of replacing the clipboard, unless those previously cut lines have been already pasted or the last cut was more than 10 seconds ago. This last bit doesn't really work: newly cut lines are appended to the clipboard regardless of when was the last cut. So fix it. --- internal/action/actions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 5cb177ea..28337d72 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1201,7 +1201,7 @@ func (h *BufPane) CutLine() bool { if !h.Cursor.HasSelection() { return false } - if h.freshClip { + if h.freshClip && time.Since(h.lastCutTime) < 10*time.Second { if h.Cursor.HasSelection() { if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { InfoBar.Error(err) @@ -1209,7 +1209,7 @@ func (h *BufPane) CutLine() bool { clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) } } - } else if time.Since(h.lastCutTime)/time.Second > 10*time.Second || !h.freshClip { + } else { h.Copy() } h.freshClip = true From 2860efbe3ab163b3210f4db2ef30dffcbf13a2ef Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 12:16:25 +0200 Subject: [PATCH 05/17] CutLine: remove unneeded if check --- internal/action/actions.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 28337d72..658e6c41 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1202,12 +1202,10 @@ func (h *BufPane) CutLine() bool { return false } if h.freshClip && time.Since(h.lastCutTime) < 10*time.Second { - if h.Cursor.HasSelection() { - if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { - InfoBar.Error(err) - } else { - clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) - } + if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { + InfoBar.Error(err) + } else { + clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) } } else { h.Copy() From 830768b7159575ecf1243fde3d1f31f1e8a6b3da Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 01:53:45 +0200 Subject: [PATCH 06/17] Reorganize Copy and CopyLine actions Make Copy return false if there is no selection, and change the default binding for Ctrl-c from CopyLine|Copy to Copy|CopyLine accordingly, to make the semantics more meaningful: copying selection always fails if there is no selection. --- internal/action/actions.go | 12 +++++------- internal/action/defaults_darwin.go | 4 ++-- internal/action/defaults_other.go | 4 ++-- runtime/help/keybindings.md | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 658e6c41..222e83a8 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1164,20 +1164,18 @@ func (h *BufPane) Redo() bool { // Copy the selection to the system clipboard func (h *BufPane) Copy() bool { - if h.Cursor.HasSelection() { - h.Cursor.CopySelection(clipboard.ClipboardReg) - h.freshClip = true - InfoBar.Message("Copied selection") + if !h.Cursor.HasSelection() { + return false } + h.Cursor.CopySelection(clipboard.ClipboardReg) + h.freshClip = true + InfoBar.Message("Copied selection") h.Relocate() return true } // CopyLine copies the current line to the clipboard func (h *BufPane) CopyLine() bool { - if h.Cursor.HasSelection() { - return false - } origLoc := h.Cursor.Loc origLastVisualX := h.Cursor.LastVisualX h.Cursor.SelectLine() diff --git a/internal/action/defaults_darwin.go b/internal/action/defaults_darwin.go index e1a54b79..21d6d2b4 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -45,7 +45,7 @@ var bufdefaults = map[string]string{ "Alt-]": "DiffNext|CursorEnd", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", + "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut", "Ctrl-k": "CutLine", "Ctrl-d": "DuplicateLine", @@ -144,7 +144,7 @@ var infodefaults = map[string]string{ "Backtab": "CycleAutocompleteBack", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", + "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", diff --git a/internal/action/defaults_other.go b/internal/action/defaults_other.go index a932688a..0e6a54b5 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -48,7 +48,7 @@ var bufdefaults = map[string]string{ "Alt-]": "DiffNext|CursorEnd", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", + "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut", "Ctrl-k": "CutLine", "Ctrl-d": "DuplicateLine", @@ -147,7 +147,7 @@ var infodefaults = map[string]string{ "Backtab": "CycleAutocompleteBack", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", + "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index 17f9ab35..8926a542 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -491,7 +491,7 @@ conventions for text editing defaults. "Alt-]": "DiffNext|CursorEnd", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", + "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut", "Ctrl-k": "CutLine", "Ctrl-d": "DuplicateLine", @@ -615,7 +615,7 @@ are given below: "Backtab": "CycleAutocompleteBack", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", + "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", From a317aefd6d1accf75d43a8f32a8838c15bf4c649 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 02:18:58 +0200 Subject: [PATCH 07/17] Reorganize Cut and CutLine actions Change behavior of the Cut action: don't implicitly call CutLine if there is no selection. Instead, make it return false in this case and change the default Ctrl-x binding to Cut|CutLine, to make it clear, explicit and in line with Copy and CopyLine actions. --- internal/action/actions.go | 2 +- internal/action/defaults_darwin.go | 4 ++-- internal/action/defaults_other.go | 4 ++-- runtime/help/keybindings.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 222e83a8..76e66f4d 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1230,7 +1230,7 @@ func (h *BufPane) Cut() bool { h.Relocate() return true } - return h.CutLine() + return false } // DuplicateLine duplicates the current line or selection diff --git a/internal/action/defaults_darwin.go b/internal/action/defaults_darwin.go index 21d6d2b4..3d5029d3 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -46,7 +46,7 @@ var bufdefaults = map[string]string{ "Ctrl-z": "Undo", "Ctrl-y": "Redo", "Ctrl-c": "Copy|CopyLine", - "Ctrl-x": "Cut", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-d": "DuplicateLine", "Ctrl-v": "Paste", @@ -145,7 +145,7 @@ var infodefaults = map[string]string{ "Ctrl-z": "Undo", "Ctrl-y": "Redo", "Ctrl-c": "Copy|CopyLine", - "Ctrl-x": "Cut", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", "Home": "StartOfTextToggle", diff --git a/internal/action/defaults_other.go b/internal/action/defaults_other.go index 0e6a54b5..f0fa72fc 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -49,7 +49,7 @@ var bufdefaults = map[string]string{ "Ctrl-z": "Undo", "Ctrl-y": "Redo", "Ctrl-c": "Copy|CopyLine", - "Ctrl-x": "Cut", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-d": "DuplicateLine", "Ctrl-v": "Paste", @@ -148,7 +148,7 @@ var infodefaults = map[string]string{ "Ctrl-z": "Undo", "Ctrl-y": "Redo", "Ctrl-c": "Copy|CopyLine", - "Ctrl-x": "Cut", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", "Home": "StartOfTextToggle", diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index 8926a542..ac7046d3 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -492,7 +492,7 @@ conventions for text editing defaults. "Ctrl-z": "Undo", "Ctrl-y": "Redo", "Ctrl-c": "Copy|CopyLine", - "Ctrl-x": "Cut", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-d": "DuplicateLine", "Ctrl-v": "Paste", @@ -616,7 +616,7 @@ are given below: "Ctrl-z": "Undo", "Ctrl-y": "Redo", "Ctrl-c": "Copy|CopyLine", - "Ctrl-x": "Cut", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", "Home": "StartOfTextToggle", From c1bbd7b041892ba0383e49043db74fbde9d4949b Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 12:25:25 +0200 Subject: [PATCH 08/17] CutLine: cosmetic refactoring --- internal/action/actions.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 76e66f4d..805bf739 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1220,17 +1220,17 @@ func (h *BufPane) CutLine() bool { // Cut the selection to the system clipboard func (h *BufPane) Cut() bool { - if h.Cursor.HasSelection() { - h.Cursor.CopySelection(clipboard.ClipboardReg) - h.Cursor.DeleteSelection() - h.Cursor.ResetSelection() - h.freshClip = true - InfoBar.Message("Cut selection") - - h.Relocate() - return true + if !h.Cursor.HasSelection() { + return false } - return false + h.Cursor.CopySelection(clipboard.ClipboardReg) + h.Cursor.DeleteSelection() + h.Cursor.ResetSelection() + h.freshClip = true + InfoBar.Message("Cut selection") + + h.Relocate() + return true } // DuplicateLine duplicates the current line or selection From 9f7bdb109b7984501254c94d4808efdab1fd8657 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 12:28:32 +0200 Subject: [PATCH 09/17] Cosmetic change: move Cut above CutLine --- internal/action/actions.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 805bf739..94fce619 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1193,6 +1193,21 @@ func (h *BufPane) CopyLine() bool { return true } +// Cut the selection to the system clipboard +func (h *BufPane) Cut() bool { + if !h.Cursor.HasSelection() { + return false + } + h.Cursor.CopySelection(clipboard.ClipboardReg) + h.Cursor.DeleteSelection() + h.Cursor.ResetSelection() + h.freshClip = true + InfoBar.Message("Cut selection") + + h.Relocate() + return true +} + // CutLine cuts the current line to the clipboard func (h *BufPane) CutLine() bool { h.Cursor.SelectLine() @@ -1218,21 +1233,6 @@ func (h *BufPane) CutLine() bool { return true } -// Cut the selection to the system clipboard -func (h *BufPane) Cut() bool { - if !h.Cursor.HasSelection() { - return false - } - h.Cursor.CopySelection(clipboard.ClipboardReg) - h.Cursor.DeleteSelection() - h.Cursor.ResetSelection() - h.freshClip = true - InfoBar.Message("Cut selection") - - h.Relocate() - return true -} - // DuplicateLine duplicates the current line or selection func (h *BufPane) DuplicateLine() bool { var infoMessage = "Duplicated line" From fdacb289624a5b6568571cc443e60b201ef5212f Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 12:59:19 +0200 Subject: [PATCH 10/17] CopyLine, CutLine, DeleteLine: respect selection When there is a selection containing multiple lines, CutLine, DeleteLine and CopyLine actions currently cut/delete/copy just the "current" line, as usual. This behavior is at least confusing, since when there is a selection, the cursor is not displayed, so the user doesn't know which line is the current one. So change the behavior. When there is a multi-line selection, cut/delete/copy all lines covered by the selection, not just the current line. Note that it will cut/delete/copy whole lines, not just the selection itself, i.e. if the first and/or the last line of the selection is only partially within the selection, we will cut/delete/copy the entire first and last lines nonetheless. --- internal/action/actions.go | 63 ++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 94fce619..b0a959ba 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1162,6 +1162,26 @@ func (h *BufPane) Redo() bool { return true } +func (h *BufPane) selectLines() int { + if h.Cursor.HasSelection() { + start := h.Cursor.CurSelection[0] + end := h.Cursor.CurSelection[1] + if start.GreaterThan(end) { + start, end = end, start + } + if end.X == 0 { + end = end.Move(-1, h.Buf) + } + + h.Cursor.Deselect(true) + h.Cursor.SetSelectionStart(buffer.Loc{0, start.Y}) + h.Cursor.SetSelectionEnd(buffer.Loc{0, end.Y + 1}) + } else { + h.Cursor.SelectLine() + } + return h.Cursor.CurSelection[1].Y - h.Cursor.CurSelection[0].Y +} + // Copy the selection to the system clipboard func (h *BufPane) Copy() bool { if !h.Cursor.HasSelection() { @@ -1174,21 +1194,28 @@ func (h *BufPane) Copy() bool { return true } -// CopyLine copies the current line to the clipboard +// CopyLine copies the current line to the clipboard. If there is a selection, +// CopyLine copies all the lines that are (fully or partially) in the selection. func (h *BufPane) CopyLine() bool { origLoc := h.Cursor.Loc origLastVisualX := h.Cursor.LastVisualX - h.Cursor.SelectLine() - if !h.Cursor.HasSelection() { + origSelection := h.Cursor.CurSelection + + nlines := h.selectLines() + if nlines == 0 { return false } h.Cursor.CopySelection(clipboard.ClipboardReg) h.freshClip = true - InfoBar.Message("Copied line") + if nlines > 1 { + InfoBar.Message(fmt.Sprintf("Copied %d lines", nlines)) + } else { + InfoBar.Message("Copied line") + } - h.Cursor.Deselect(true) h.Cursor.Loc = origLoc h.Cursor.LastVisualX = origLastVisualX + h.Cursor.CurSelection = origSelection h.Relocate() return true } @@ -1208,10 +1235,11 @@ func (h *BufPane) Cut() bool { return true } -// CutLine cuts the current line to the clipboard +// CutLine cuts the current line to the clipboard. If there is a selection, +// CutLine cuts all the lines that are (fully or partially) in the selection. func (h *BufPane) CutLine() bool { - h.Cursor.SelectLine() - if !h.Cursor.HasSelection() { + nlines := h.selectLines() + if nlines == 0 { return false } if h.freshClip && time.Since(h.lastCutTime) < 10*time.Second { @@ -1228,7 +1256,11 @@ func (h *BufPane) CutLine() bool { h.Cursor.DeleteSelection() h.Cursor.ResetSelection() h.Cursor.StoreVisualX() - InfoBar.Message("Cut line") + if nlines > 1 { + InfoBar.Message(fmt.Sprintf("Cut %d lines", nlines)) + } else { + InfoBar.Message("Cut line") + } h.Relocate() return true } @@ -1250,16 +1282,21 @@ func (h *BufPane) DuplicateLine() bool { return true } -// DeleteLine deletes the current line +// DeleteLine deletes the current line. If there is a selection, DeleteLine +// deletes all the lines that are (fully or partially) in the selection. func (h *BufPane) DeleteLine() bool { - h.Cursor.SelectLine() - if !h.Cursor.HasSelection() { + nlines := h.selectLines() + if nlines == 0 { return false } h.Cursor.DeleteSelection() h.Cursor.ResetSelection() h.Cursor.StoreVisualX() - InfoBar.Message("Deleted line") + if nlines > 1 { + InfoBar.Message(fmt.Sprintf("Deleted %d lines", nlines)) + } else { + InfoBar.Message("Deleted line") + } h.Relocate() return true } From e6825f0e084e662bd3a79bcec96c874308aa8332 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 13:14:02 +0200 Subject: [PATCH 11/17] CutLine: make infobar message more useful Since CutLine may add lines to the clipboard instead of replacing the clipboard, improve its info message to show how many lines are in the clipboard in total, not just how many lines were added to it last time. --- internal/action/actions.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index b0a959ba..9e9cb8b8 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1242,11 +1242,13 @@ func (h *BufPane) CutLine() bool { if nlines == 0 { return false } + totalLines := nlines if h.freshClip && time.Since(h.lastCutTime) < 10*time.Second { if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { InfoBar.Error(err) } else { clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) + totalLines = strings.Count(clip, "\n") + nlines } } else { h.Copy() @@ -1256,8 +1258,8 @@ func (h *BufPane) CutLine() bool { h.Cursor.DeleteSelection() h.Cursor.ResetSelection() h.Cursor.StoreVisualX() - if nlines > 1 { - InfoBar.Message(fmt.Sprintf("Cut %d lines", nlines)) + if totalLines > 1 { + InfoBar.Message(fmt.Sprintf("Cut %d lines", totalLines)) } else { InfoBar.Message("Cut line") } From 04143c7a890d992fb1fcd0733c18f5e7c3e2ba79 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 14:58:05 +0200 Subject: [PATCH 12/17] Make Cut, Copy, CopyLine don't mess with CutLine's multi line cuts Weird behavior is observed e.g. if we cut some lines with CutLine, then copy some selection with Copy, then cut some other lines with CutLine, and then paste. The pasted cliboard contains not just the lines that were cut at the last step, but also the selection that was copied before that. Fix that by resetting the CutLine's repeated line cuts whenever we copy anything to the clipboard via any other action (Cut, Copy or CopyLine). --- internal/action/actions.go | 8 ++++---- internal/action/bufpane.go | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 9e9cb8b8..89821beb 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1188,7 +1188,7 @@ func (h *BufPane) Copy() bool { return false } h.Cursor.CopySelection(clipboard.ClipboardReg) - h.freshClip = true + h.freshClip = false InfoBar.Message("Copied selection") h.Relocate() return true @@ -1206,7 +1206,7 @@ func (h *BufPane) CopyLine() bool { return false } h.Cursor.CopySelection(clipboard.ClipboardReg) - h.freshClip = true + h.freshClip = false if nlines > 1 { InfoBar.Message(fmt.Sprintf("Copied %d lines", nlines)) } else { @@ -1228,7 +1228,7 @@ func (h *BufPane) Cut() bool { h.Cursor.CopySelection(clipboard.ClipboardReg) h.Cursor.DeleteSelection() h.Cursor.ResetSelection() - h.freshClip = true + h.freshClip = false InfoBar.Message("Cut selection") h.Relocate() @@ -1251,7 +1251,7 @@ func (h *BufPane) CutLine() bool { totalLines = strings.Count(clip, "\n") + nlines } } else { - h.Copy() + h.Cursor.CopySelection(clipboard.ClipboardReg) } h.freshClip = true h.lastCutTime = time.Now() diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 7b348b79..6a9e0e09 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -235,7 +235,8 @@ type BufPane struct { // It is used for clearing the clipboard to replace it with fresh cut lines. lastCutTime time.Time - // freshClip returns true if the clipboard has never been pasted. + // freshClip returns true if one or more lines have been cut to the clipboard + // and have never been pasted yet. freshClip bool // Was the last mouse event actually a double click? From 33a1bb120f70b9139fff14995d2acda51d7eac9c Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 15:04:43 +0200 Subject: [PATCH 13/17] CutLine: return if cliboard read failed If we ever encounter this clipboard.Read() failure, return false immediately. Otherwise, InfoBar.Error(err) will have no effect (it will be immediately overwritten by InfoBar.Message()) so we won't even know that there was an error. --- internal/action/actions.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/action/actions.go b/internal/action/actions.go index 89821beb..1a45d8f6 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1246,6 +1246,7 @@ func (h *BufPane) CutLine() bool { if h.freshClip && time.Since(h.lastCutTime) < 10*time.Second { if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { InfoBar.Error(err) + return false } else { clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) totalLines = strings.Count(clip, "\n") + nlines From 25f71eec2d3968282c9daed0753c3ac7a69ef483 Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sun, 9 Jun 2024 15:21:32 +0200 Subject: [PATCH 14/17] DuplicateLine: move selection duplication to separate Duplicate action - Add a new Duplicate action which just duplicates the selection (and returns false if there is no selection). - Change the behavior of the DuplicateLine action to only duplicate the current line, not the selection. - Change the default action bound to Ctrl-d from DuplicateLine to Duplicate|DuplicateLine, so that the default behavior doesn't change. This allows the user to rebind keybindings in a more flexible way, i.e. to choose whether a key should duplicate just lines, or just selections, or both, - in a similar fashion to Copy, Cut, Delete actions. --- internal/action/actions.go | 25 ++++++++++++++----------- internal/action/bufpane.go | 2 ++ internal/action/defaults_darwin.go | 2 +- internal/action/defaults_other.go | 2 +- runtime/help/keybindings.md | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 1a45d8f6..f8a8624a 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1268,19 +1268,22 @@ func (h *BufPane) CutLine() bool { return true } -// DuplicateLine duplicates the current line or selection -func (h *BufPane) DuplicateLine() bool { - var infoMessage = "Duplicated line" - if h.Cursor.HasSelection() { - infoMessage = "Duplicated selection" - h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection())) - } else { - h.Cursor.End() - h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y))) - // h.Cursor.Right() +// Duplicate the selection +func (h *BufPane) Duplicate() bool { + if !h.Cursor.HasSelection() { + return false } + h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection())) + InfoBar.Message("Duplicated selection") + h.Relocate() + return true +} - InfoBar.Message(infoMessage) +// DuplicateLine duplicates the current line +func (h *BufPane) DuplicateLine() bool { + h.Cursor.End() + h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y))) + InfoBar.Message("Duplicated line") h.Relocate() return true } diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 6a9e0e09..4d17816e 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -784,6 +784,7 @@ var BufKeyActions = map[string]BufKeyAction{ "CopyLine": (*BufPane).CopyLine, "Cut": (*BufPane).Cut, "CutLine": (*BufPane).CutLine, + "Duplicate": (*BufPane).Duplicate, "DuplicateLine": (*BufPane).DuplicateLine, "DeleteLine": (*BufPane).DeleteLine, "MoveLinesUp": (*BufPane).MoveLinesUp, @@ -910,6 +911,7 @@ var MultiActions = map[string]bool{ "Copy": true, "Cut": true, "CutLine": true, + "Duplicate": true, "DuplicateLine": true, "DeleteLine": true, "MoveLinesUp": true, diff --git a/internal/action/defaults_darwin.go b/internal/action/defaults_darwin.go index 3d5029d3..b04cedd9 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -48,7 +48,7 @@ var bufdefaults = map[string]string{ "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", - "Ctrl-d": "DuplicateLine", + "Ctrl-d": "Duplicate|DuplicateLine", "Ctrl-v": "Paste", "Ctrl-a": "SelectAll", "Ctrl-t": "AddTab", diff --git a/internal/action/defaults_other.go b/internal/action/defaults_other.go index f0fa72fc..816256a7 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -51,7 +51,7 @@ var bufdefaults = map[string]string{ "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", - "Ctrl-d": "DuplicateLine", + "Ctrl-d": "Duplicate|DuplicateLine", "Ctrl-v": "Paste", "Ctrl-a": "SelectAll", "Ctrl-t": "AddTab", diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index ac7046d3..fb463d68 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -494,7 +494,7 @@ conventions for text editing defaults. "Ctrl-c": "Copy|CopyLine", "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", - "Ctrl-d": "DuplicateLine", + "Ctrl-d": "Duplicate|DuplicateLine", "Ctrl-v": "Paste", "Ctrl-a": "SelectAll", "Ctrl-t": "AddTab", From 6f724bc4247479bd195c35f39f6f11891644b90d Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Sat, 8 Jun 2024 23:57:57 +0200 Subject: [PATCH 15/17] DuplicateLine: respect selections Similarly to CutLine, DeleteLine and CopyLine actions, if there is a selection, duplicate not just the current line but all the lines covered (fully or partially) by the selection. --- internal/action/actions.go | 41 ++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index f8a8624a..c3b4b150 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1279,11 +1279,44 @@ func (h *BufPane) Duplicate() bool { return true } -// DuplicateLine duplicates the current line +// DuplicateLine duplicates the current line. If there is a selection, DuplicateLine +// duplicates all the lines that are (fully or partially) in the selection. func (h *BufPane) DuplicateLine() bool { - h.Cursor.End() - h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y))) - InfoBar.Message("Duplicated line") + if h.Cursor.HasSelection() { + origLoc := h.Cursor.Loc + origLastVisualX := h.Cursor.LastVisualX + origSelection := h.Cursor.CurSelection + + start := h.Cursor.CurSelection[0] + end := h.Cursor.CurSelection[1] + if start.GreaterThan(end) { + start, end = end, start + } + if end.X == 0 { + end = end.Move(-1, h.Buf) + } + + h.Cursor.Deselect(true) + h.Cursor.Loc = end + h.Cursor.End() + for y := start.Y; y <= end.Y; y++ { + h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(y))) + } + + h.Cursor.Loc = origLoc + h.Cursor.LastVisualX = origLastVisualX + h.Cursor.CurSelection = origSelection + + if start.Y < end.Y { + InfoBar.Message(fmt.Sprintf("Duplicated %d lines", end.Y-start.Y+1)) + } else { + InfoBar.Message("Duplicated line") + } + } else { + h.Cursor.End() + h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y))) + InfoBar.Message("Duplicated line") + } h.Relocate() return true } From 68d6f43c638673960057117efeb24b945afabf5a Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Wed, 12 Jun 2024 03:16:36 +0200 Subject: [PATCH 16/17] CutLine: remove lastCutTime feature The lastCutTime feature (reset the clipboard instead of appending to the clipboard if the last CutLine was more than 10 seconds ago) was implemented 8 years ago but was always buggy and never really worked, until we have accidentally found and fixed the bug just now. No one ever complained or noticed that, which means it is not a very useful feature. Fixing it changes the existing behavior (essentially adds a new feature which did not really exist before) and there is no reason to assume that this new behavior will be welcome by users. So it's better to remove this feature. --- internal/action/actions.go | 3 +-- internal/action/bufpane.go | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index c3b4b150..cca43bfd 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1243,7 +1243,7 @@ func (h *BufPane) CutLine() bool { return false } totalLines := nlines - if h.freshClip && time.Since(h.lastCutTime) < 10*time.Second { + if h.freshClip { if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { InfoBar.Error(err) return false @@ -1255,7 +1255,6 @@ func (h *BufPane) CutLine() bool { h.Cursor.CopySelection(clipboard.ClipboardReg) } h.freshClip = true - h.lastCutTime = time.Now() h.Cursor.DeleteSelection() h.Cursor.ResetSelection() h.Cursor.StoreVisualX() diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 4d17816e..4f213751 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -231,10 +231,6 @@ type BufPane struct { lastClickTime time.Time lastLoc buffer.Loc - // lastCutTime stores when the last ctrl+k was issued. - // It is used for clearing the clipboard to replace it with fresh cut lines. - lastCutTime time.Time - // freshClip returns true if one or more lines have been cut to the clipboard // and have never been pasted yet. freshClip bool From bf6584739f21f37af3160f8e1081d27555dad65b Mon Sep 17 00:00:00 2001 From: Dmytro Maluka Date: Fri, 14 Jun 2024 00:49:51 +0200 Subject: [PATCH 17/17] help/keybindings: document CutLine behavior --- runtime/help/keybindings.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index fb463d68..3178664d 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -274,6 +274,14 @@ Autocomplete The `StartOfTextToggle` and `SelectToStartOfTextToggle` actions toggle between jumping to the start of the text (first) and start of the line. +The `CutLine` action cuts the current line and adds it to the previously cut +lines in the clipboard since the last paste (rather than just replaces the +clipboard contents with this line). So you can cut multiple, not necessarily +consecutive lines to the clipboard just by pressing `Ctrl-k` multiple times, +without selecting them. If you want the more traditional behavior i.e. just +rewrite the clipboard every time, you can use `CopyLine,DeleteLine` action +instead of `CutLine`. + You can also bind some mouse actions (these must be bound to mouse buttons) ```