Skip to content

Commit

Permalink
Merge branch 'copy-cut-paste'
Browse files Browse the repository at this point in the history
  • Loading branch information
itchyny committed Apr 15, 2018
2 parents 7f186e0 + 8138291 commit c39eb95
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 1 deletion.
19 changes: 19 additions & 0 deletions editor/editor.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"sync"

"github.com/itchyny/bed/buffer"
"github.com/itchyny/bed/event"
"github.com/itchyny/bed/mode"
"github.com/itchyny/bed/state"
Expand All @@ -20,6 +21,7 @@ type Editor struct {
searchTarget string
searchMode rune
prevEventType event.Type
buffer *buffer.Buffer
err error
errtyp int
eventCh chan event.Event
Expand Down Expand Up @@ -103,6 +105,17 @@ func (e *Editor) emit(ev event.Event) (redraw bool, finish bool) {
width, height := e.ui.Size()
e.wm.Resize(width, height-1)
redraw = true
case event.Copied:
e.buffer, e.mode, e.prevMode = ev.Buffer, mode.Normal, e.mode
if l, err := e.buffer.Len(); err != nil {
e.err, e.errtyp = err, state.MessageError
} else {
e.err, e.errtyp = fmt.Errorf("%d (0x%x) bytes %s", l, l, ev.Arg), state.MessageInfo
}
redraw = true
case event.Pasted:
e.err, e.errtyp = fmt.Errorf("%d (0x%x) bytes pasted", ev.Count, ev.Count), state.MessageInfo
redraw = true
default:
switch ev.Type {
case event.StartInsert, event.StartInsertHead, event.StartAppend, event.StartAppendEnd:
Expand Down Expand Up @@ -141,6 +154,12 @@ func (e *Editor) emit(ev event.Event) (redraw bool, finish bool) {
ev.Arg, ev.Rune = e.searchTarget, e.searchMode
case event.PreviousSearch:
ev.Arg, ev.Rune = e.searchTarget, e.searchMode
case event.Paste, event.PastePrev:
if e.buffer == nil {
e.mu.Unlock()
return
}
ev.Buffer = e.buffer
}
if e.mode == mode.Cmdline || e.mode == mode.Search ||
ev.Type == event.ExitCmdline || ev.Type == event.ExecuteCmdline {
Expand Down
54 changes: 54 additions & 0 deletions editor/editor_test.go
Expand Up @@ -310,3 +310,57 @@ func TestEditorCmdlineQuit(t *testing.T) {
t.Errorf("err should be nil but got: %v", err)
}
}

func TestEditorCopyCutPaste(t *testing.T) {
f1, _ := ioutil.TempFile("", "bed-test-editor-copy-cut-paste1")
f2, _ := ioutil.TempFile("", "bed-test-editor-copy-cut-paste2")
defer os.Remove(f1.Name())
defer os.Remove(f2.Name())
str := "Hello, world!"
_, _ = f1.WriteString(str)
_ = f1.Close()
_ = f2.Close()
ui := newTestUI()
editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline())
if err := editor.Init(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.Open(f1.Name()); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
go func() {
for _, e := range []struct {
typ event.Type
ch rune
count int64
arg string
}{
{event.CursorNext, 'w', 2, ""}, {event.StartVisual, 'v', 0, ""},
{event.CursorNext, 'w', 5, ""}, {event.Copy, 'y', 0, ""},
{event.CursorNext, 'w', 3, ""}, {event.Paste, 'p', 0, ""},
{event.CursorPrev, 'b', 2, ""}, {event.StartVisual, 'v', 0, ""},
{event.CursorPrev, 'b', 5, ""}, {event.Cut, 'd', 0, ""},
{event.CursorNext, 'w', 5, ""}, {event.PastePrev, 'P', 0, ""},
{event.Write, 'w', 0, f2.Name()},
} {
time.Sleep(50 * time.Millisecond)
ui.Emit(event.Event{Type: e.typ, Rune: e.ch, Count: e.count, Arg: e.arg})
}
time.Sleep(500 * time.Millisecond)
ui.Emit(event.Event{Type: event.Quit})
}()
if err := editor.Run(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
if err := editor.err; !strings.HasSuffix(err.Error(), "19 (0x13) bytes written") {
t.Errorf("err should be ends with %q but got: %v", "19 (0x13) bytes written", err)
}
if err := editor.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
bs, _ := ioutil.ReadFile(f2.Name())
expected := "Hell w woo,llo,rld!"
if string(bs) != expected {
t.Errorf("file contents should be %q but got %q", expected, string(bs))
}
}
5 changes: 5 additions & 0 deletions editor/key.go
Expand Up @@ -52,6 +52,9 @@ func defaultKeyManagers() map[mode.Mode]*key.Manager {
km.Register(event.Decrement, "c-x")
km.Register(event.Decrement, "-")

km.Register(event.Paste, "p")
km.Register(event.PastePrev, "P")

km.Register(event.StartInsert, "i")
km.Register(event.StartInsertHead, "I")
km.Register(event.StartAppend, "a")
Expand Down Expand Up @@ -157,6 +160,8 @@ func defaultKeyManagers() map[mode.Mode]*key.Manager {
km.Register(event.PageEnd, "G")
km.Register(event.SwitchFocus, "tab")
km.Register(event.SwitchFocus, "backtab")
km.Register(event.Copy, "y")
km.Register(event.Cut, "d")
kms[mode.Visual] = km

km = key.NewManager(false)
Expand Down
13 changes: 12 additions & 1 deletion event/event.go
@@ -1,6 +1,9 @@
package event

import "github.com/itchyny/bed/mode"
import (
"github.com/itchyny/bed/buffer"
"github.com/itchyny/bed/mode"
)

// Event represents the event emitted by UI.
type Event struct {
Expand All @@ -12,6 +15,7 @@ type Event struct {
Arg string
Error error
Mode mode.Mode
Buffer *buffer.Buffer
}

// Type ...
Expand Down Expand Up @@ -70,6 +74,13 @@ const (
SwitchVisualEnd
ExitVisual

Copy
Cut
Copied
Paste
PastePrev
Pasted

StartCmdlineCommand
StartCmdlineSearchForward
StartCmdlineSearchBackward
Expand Down
12 changes: 12 additions & 0 deletions window/manager.go
Expand Up @@ -218,6 +218,18 @@ func (m *Manager) Emit(e event.Event) {
if err := m.quit(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
}
case event.Copy:
m.mu.Lock()
m.eventCh <- event.Event{Type: event.Copied, Buffer: m.windows[m.windowIndex].copy(), Arg: "yanked"}
m.mu.Unlock()
case event.Cut:
m.mu.Lock()
m.eventCh <- event.Event{Type: event.Copied, Buffer: m.windows[m.windowIndex].cut(), Arg: "deleted"}
m.mu.Unlock()
case event.Paste, event.PastePrev:
m.mu.Lock()
m.eventCh <- event.Event{Type: event.Pasted, Count: m.windows[m.windowIndex].paste(e)}
m.mu.Unlock()
case event.Write:
if err := m.write(e); err != nil {
m.eventCh <- event.Event{Type: event.Error, Error: err}
Expand Down
107 changes: 107 additions & 0 deletions window/manager_test.go
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"github.com/itchyny/bed/buffer"
"github.com/itchyny/bed/event"
"github.com/itchyny/bed/layout"
"github.com/itchyny/bed/mode"
Expand Down Expand Up @@ -204,3 +205,109 @@ func TestManagerWincmd(t *testing.T) {

wm.Close()
}

func TestManagerCopyCutPaste(t *testing.T) {
wm := NewManager()
eventCh, redrawCh, waitCh := make(chan event.Event), make(chan struct{}), make(chan struct{})
wm.Init(eventCh, redrawCh)
f, err := ioutil.TempFile("", "bed-test-manager-copy-cut-paste")
str := "Hello, world!"
_, err = f.WriteString(str)
if err != nil {
t.Errorf("err should be nil but got %v", err)
}
if err := f.Close(); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
defer os.Remove(f.Name())
wm.SetSize(110, 20)
if err := wm.Open(f.Name()); err != nil {
t.Errorf("err should be nil but got: %v", err)
}
_, _, _, _ = wm.State()
go func() {
<-redrawCh
<-redrawCh
<-redrawCh
waitCh <- struct{}{}
ev := <-eventCh
if ev.Type != event.Copied {
t.Errorf("event type should be %d but got: %d", event.Copied, ev.Type)
}
if ev.Buffer == nil {
t.Errorf("Buffer should not be nil but got: %#v", ev)
}
if ev.Arg != "yanked" {
t.Errorf("Arg should be %q but got: %q", "yanked", ev.Arg)
}
p := make([]byte, 20)
_, _ = ev.Buffer.ReadAt(p, 0)
if !strings.HasPrefix(string(p), "lo, worl") {
t.Errorf("buffer string should be %q but got: %q", "", string(p))
}
waitCh <- struct{}{}
<-redrawCh
<-redrawCh
waitCh <- struct{}{}
ev = <-eventCh
if ev.Type != event.Copied {
t.Errorf("event type should be %d but got: %d", event.Copied, ev.Type)
}
if ev.Buffer == nil {
t.Errorf("Buffer should not be nil but got: %#v", ev)
}
if ev.Arg != "deleted" {
t.Errorf("Arg should be %q but got: %q", "deleted", ev.Arg)
}
p = make([]byte, 20)
_, _ = ev.Buffer.ReadAt(p, 0)
if !strings.HasPrefix(string(p), "lo, wo") {
t.Errorf("buffer string should be %q but got: %q", "", string(p))
}
windowStates, _, _, _ := wm.State()
ws := windowStates[0]
if ws.Length != int64(7) {
t.Errorf("Length should be %d but got %d", int64(7), ws.Length)
}
expected := "Helrld!"
if !strings.HasPrefix(string(ws.Bytes), expected) {
t.Errorf("Bytes should start with %q but got %q", expected, string(ws.Bytes))
}
waitCh <- struct{}{}
<-redrawCh
waitCh <- struct{}{}
ev = <-eventCh
if ev.Type != event.Pasted {
t.Errorf("event type should be %d but got: %d", event.Pasted, ev.Type)
}
if ev.Count != 18 {
t.Errorf("Count should be %d but got: %d", 18, ev.Count)
}
windowStates, _, _, _ = wm.State()
ws = windowStates[0]
if ws.Length != int64(25) {
t.Errorf("Length should be %d but got %d", int64(25), ws.Length)
}
expected = "Hefoobarfoobarfoobarlrld!"
if !strings.HasPrefix(string(ws.Bytes), expected) {
t.Errorf("Bytes should start with %q but got %q", expected, string(ws.Bytes))
}
close(waitCh)
}()
wm.Emit(event.Event{Type: event.CursorNext, Mode: mode.Normal, Count: 3})
wm.Emit(event.Event{Type: event.StartVisual})
wm.Emit(event.Event{Type: event.CursorNext, Mode: mode.Visual, Count: 7})
<-waitCh
wm.Emit(event.Event{Type: event.Copy})
<-waitCh
wm.Emit(event.Event{Type: event.StartVisual})
wm.Emit(event.Event{Type: event.CursorNext, Mode: mode.Visual, Count: 5})
<-waitCh
wm.Emit(event.Event{Type: event.Cut})
<-waitCh
wm.Emit(event.Event{Type: event.CursorPrev, Mode: mode.Normal, Count: 2})
<-waitCh
wm.Emit(event.Event{Type: event.Paste, Buffer: buffer.NewBuffer(strings.NewReader("foobar")), Count: 3})
<-waitCh
wm.Close()
}
64 changes: 64 additions & 0 deletions window/window.go
Expand Up @@ -801,6 +801,70 @@ func (w *window) exitVisual() {
w.visualStart = -1
}

func (w *window) copy() *buffer.Buffer {
w.mu.Lock()
defer w.mu.Unlock()
if w.visualStart < 0 {
panic("window#copy should be called in visual mode")
}
start, end := w.visualStart, w.cursor
if start > end {
start, end = end, start
}
w.cursor = w.visualStart
w.visualStart = -1
if w.cursor < w.offset {
w.offset = w.cursor / w.width * w.width
} else if w.cursor >= w.offset+w.height*w.width {
w.offset = (w.cursor - w.height*w.width + w.width) / w.width * w.width
}
return w.buffer.Copy(start, end+1)
}

func (w *window) cut() *buffer.Buffer {
w.mu.Lock()
defer w.mu.Unlock()
if w.visualStart < 0 {
panic("window#cut should be called in visual mode")
}
start, end := w.visualStart, w.cursor
if start > end {
start, end = end, start
}
w.visualStart = -1
b := w.buffer.Copy(start, end+1)
w.buffer.Cut(start, end+1)
w.length, _ = w.buffer.Len()
w.cursor = mathutil.MinInt64(start, mathutil.MaxInt64(w.length, 1)-1)
if w.cursor < w.offset {
w.offset = w.cursor / w.width * w.width
} else if w.cursor >= w.offset+w.height*w.width {
w.offset = (w.cursor - w.height*w.width + w.width) / w.width * w.width
}
return b
}

func (w *window) paste(e event.Event) int64 {
w.mu.Lock()
defer w.mu.Unlock()
count := mathutil.MaxInt64(e.Count, 1)
pos := w.cursor
if e.Type != event.PastePrev {
pos = mathutil.MinInt64(w.cursor+1, w.length)
}
for i := int64(0); i < count; i++ {
w.buffer.Paste(pos, e.Buffer)
}
l, _ := e.Buffer.Len()
w.length, _ = w.buffer.Len()
w.cursor = pos + mathutil.MinInt64(l*count-1, mathutil.MaxInt64(w.length, 1)-1-pos)
if w.cursor >= w.offset+w.height*w.width {
w.offset = (w.cursor - w.height*w.width + w.width) / w.width * w.width
}
w.changedTick++
return l * count
}

func (w *window) search(str string, forward bool) {
if forward {
w.searchForward(str)
Expand Down

0 comments on commit c39eb95

Please sign in to comment.