diff --git a/internal/action/actions.go b/internal/action/actions.go index 79bff07d74..cca43bfdcb 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1162,101 +1162,179 @@ 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() { - 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 = false + InfoBar.Message("Copied selection") h.Relocate() 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 { - if h.Cursor.HasSelection() { + origLoc := h.Cursor.Loc + origLastVisualX := h.Cursor.LastVisualX + origSelection := h.Cursor.CurSelection + + nlines := h.selectLines() + if nlines == 0 { return false } - origLoc := h.Cursor.Loc - h.Cursor.SelectLine() h.Cursor.CopySelection(clipboard.ClipboardReg) - h.freshClip = true - InfoBar.Message("Copied line") + h.freshClip = false + 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 } -// CutLine cuts the current line to the clipboard -func (h *BufPane) CutLine() bool { - h.Cursor.SelectLine() +// 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 = false + InfoBar.Message("Cut selection") + + h.Relocate() + return true +} + +// 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 { + nlines := h.selectLines() + if nlines == 0 { + return false + } + totalLines := nlines if h.freshClip { - 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) + return false + } else { + clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) + totalLines = strings.Count(clip, "\n") + nlines } - } else if time.Since(h.lastCutTime)/time.Second > 10*time.Second || !h.freshClip { - h.Copy() + } else { + h.Cursor.CopySelection(clipboard.ClipboardReg) } h.freshClip = true - h.lastCutTime = time.Now() h.Cursor.DeleteSelection() h.Cursor.ResetSelection() - InfoBar.Message("Cut line") + h.Cursor.StoreVisualX() + if totalLines > 1 { + InfoBar.Message(fmt.Sprintf("Cut %d lines", totalLines)) + } else { + InfoBar.Message("Cut line") + } h.Relocate() return true } -// 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 +// Duplicate the selection +func (h *BufPane) Duplicate() bool { + if !h.Cursor.HasSelection() { + return false } - return h.CutLine() + h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection())) + InfoBar.Message("Duplicated selection") + h.Relocate() + return true } -// DuplicateLine duplicates the current line or selection +// 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 { - var infoMessage = "Duplicated line" if h.Cursor.HasSelection() { - infoMessage = "Duplicated selection" - h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection())) + 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))) - // h.Cursor.Right() + InfoBar.Message("Duplicated line") } - - InfoBar.Message(infoMessage) h.Relocate() 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() - InfoBar.Message("Deleted line") + h.Cursor.StoreVisualX() + if nlines > 1 { + InfoBar.Message(fmt.Sprintf("Deleted %d lines", nlines)) + } else { + InfoBar.Message("Deleted line") + } h.Relocate() return true } diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 7b348b79b2..4f2137519e 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -231,11 +231,8 @@ 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 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? @@ -783,6 +780,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, @@ -909,6 +907,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 e1a54b7963..b04cedd9a1 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -45,10 +45,10 @@ var bufdefaults = map[string]string{ "Alt-]": "DiffNext|CursorEnd", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", - "Ctrl-x": "Cut", + "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", @@ -144,8 +144,8 @@ var infodefaults = map[string]string{ "Backtab": "CycleAutocompleteBack", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", - "Ctrl-x": "Cut", + "Ctrl-c": "Copy|CopyLine", + "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 a932688ace..816256a796 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -48,10 +48,10 @@ var bufdefaults = map[string]string{ "Alt-]": "DiffNext|CursorEnd", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", - "Ctrl-x": "Cut", + "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", @@ -147,8 +147,8 @@ var infodefaults = map[string]string{ "Backtab": "CycleAutocompleteBack", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", - "Ctrl-x": "Cut", + "Ctrl-c": "Copy|CopyLine", + "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 17f9ab3531..3178664d8c 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) ``` @@ -491,10 +499,10 @@ conventions for text editing defaults. "Alt-]": "DiffNext|CursorEnd", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", - "Ctrl-x": "Cut", + "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", @@ -615,8 +623,8 @@ are given below: "Backtab": "CycleAutocompleteBack", "Ctrl-z": "Undo", "Ctrl-y": "Redo", - "Ctrl-c": "CopyLine|Copy", - "Ctrl-x": "Cut", + "Ctrl-c": "Copy|CopyLine", + "Ctrl-x": "Cut|CutLine", "Ctrl-k": "CutLine", "Ctrl-v": "Paste", "Home": "StartOfTextToggle",