diff --git a/internal/action/actions.go b/internal/action/actions.go index fc9f837d..475d4200 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1238,101 +1238,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") - } - 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 - h.Cursor.SelectLine() - h.Cursor.CopySelection(clipboard.ClipboardReg) - h.freshClip = true - InfoBar.Message("Copied line") - - h.Cursor.Deselect(true) - h.Cursor.Loc = origLoc - h.Relocate() - return true -} - -// CutLine cuts the current line to the clipboard -func (h *BufPane) CutLine() bool { - h.Cursor.SelectLine() if !h.Cursor.HasSelection() { return false } - 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()) - } - } - } else if time.Since(h.lastCutTime)/time.Second > 10*time.Second || !h.freshClip { - h.Copy() + h.Cursor.CopySelection(clipboard.ClipboardReg) + h.freshClip = false + InfoBar.Message("Copied selection") + h.Relocate() + return true +} + +// 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 + origSelection := h.Cursor.CurSelection + + nlines := h.selectLines() + if nlines == 0 { + return false } - h.freshClip = true - h.lastCutTime = time.Now() - h.Cursor.DeleteSelection() - h.Cursor.ResetSelection() - InfoBar.Message("Cut line") + h.Cursor.CopySelection(clipboard.ClipboardReg) + h.freshClip = false + if nlines > 1 { + InfoBar.Message(fmt.Sprintf("Copied %d lines", nlines)) + } else { + InfoBar.Message("Copied line") + } + + h.Cursor.Loc = origLoc + h.Cursor.LastVisualX = origLastVisualX + h.Cursor.CurSelection = origSelection 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 + if !h.Cursor.HasSelection() { + return false } - return h.CutLine() -} + h.Cursor.CopySelection(clipboard.ClipboardReg) + h.Cursor.DeleteSelection() + h.Cursor.ResetSelection() + h.freshClip = false + InfoBar.Message("Cut selection") -// 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() - } - - InfoBar.Message(infoMessage) h.Relocate() return true } -// DeleteLine deletes the current line -func (h *BufPane) DeleteLine() bool { - h.Cursor.SelectLine() +// 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 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 { + h.Cursor.CopySelection(clipboard.ClipboardReg) + } + h.freshClip = true + h.Cursor.DeleteSelection() + h.Cursor.ResetSelection() + h.Cursor.StoreVisualX() + if totalLines > 1 { + InfoBar.Message(fmt.Sprintf("Cut %d lines", totalLines)) + } else { + InfoBar.Message("Cut line") + } + h.Relocate() + return true +} + +// 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 +} + +// 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 { + 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 +} + +// 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 { + 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 40fb7cf1..bfb579de 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -233,11 +233,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? @@ -780,6 +777,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, @@ -907,6 +905,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 4c781af8..a18fce25 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", @@ -146,8 +146,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 d74c5096..c2afb1b8 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", @@ -149,8 +149,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 a47fdfe5..d689403b 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -278,6 +278,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) ``` @@ -495,10 +503,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", @@ -621,8 +629,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",