diff --git a/Makefile b/Makefile index de83391d..2f03e9ae 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ testgen: test: go test ./internal/... + go test ./cmd/... bench: for i in 1 2 3; do \ diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 275ff503..7c43c4a7 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -132,7 +132,7 @@ func DoPluginFlags() { // LoadInput determines which files should be loaded into buffers // based on the input stored in flag.Args() -func LoadInput() []*buffer.Buffer { +func LoadInput(args []string) []*buffer.Buffer { // There are a number of ways micro should start given its input // 1. If it is given a files in flag.Args(), it should open those @@ -147,7 +147,6 @@ func LoadInput() []*buffer.Buffer { var filename string var input []byte var err error - args := flag.Args() buffers := make([]*buffer.Buffer, 0, len(args)) btype := buffer.BTDefault @@ -262,7 +261,12 @@ func main() { DoPluginFlags() - screen.Init() + err = screen.Init() + if err != nil { + fmt.Println(err) + fmt.Println("Fatal: Micro could not initialize a Screen.") + os.Exit(1) + } defer func() { if err := recover(); err != nil { @@ -291,7 +295,8 @@ func main() { screen.TermMessage(err) } - b := LoadInput() + args := flag.Args() + b := LoadInput(args) if len(b) == 0 { // No buffers to open diff --git a/cmd/micro/micro_test.go b/cmd/micro/micro_test.go new file mode 100644 index 00000000..7029a93f --- /dev/null +++ b/cmd/micro/micro_test.go @@ -0,0 +1,341 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "testing" + + "github.com/go-errors/errors" + "github.com/stretchr/testify/assert" + "github.com/zyedidia/micro/v2/internal/action" + "github.com/zyedidia/micro/v2/internal/buffer" + "github.com/zyedidia/micro/v2/internal/config" + "github.com/zyedidia/micro/v2/internal/screen" + "github.com/zyedidia/tcell" +) + +var tempDir string +var sim tcell.SimulationScreen + +func init() { + events = make(chan tcell.Event, 8) +} + +func startup(args []string) (tcell.SimulationScreen, error) { + var err error + + tempDir, err = ioutil.TempDir("", "micro_test") + if err != nil { + return nil, err + } + err = config.InitConfigDir(tempDir) + if err != nil { + return nil, err + } + + config.InitRuntimeFiles() + err = config.ReadSettings() + if err != nil { + return nil, err + } + err = config.InitGlobalSettings() + if err != nil { + return nil, err + } + + s, err := screen.InitSimScreen() + if err != nil { + return nil, err + } + + defer func() { + if err := recover(); err != nil { + screen.Screen.Fini() + fmt.Println("Micro encountered an error:", err) + // backup all open buffers + for _, b := range buffer.OpenBuffers { + b.Backup(false) + } + // Print the stack trace too + log.Fatalf(errors.Wrap(err, 2).ErrorStack()) + os.Exit(1) + } + }() + + err = config.LoadAllPlugins() + if err != nil { + screen.TermMessage(err) + } + + action.InitBindings() + action.InitCommands() + + err = config.InitColorscheme() + if err != nil { + return nil, err + } + + b := LoadInput(args) + + if len(b) == 0 { + return nil, errors.New("No buffers opened") + } + + action.InitTabs(b) + action.InitGlobals() + + err = config.RunPluginFn("init") + if err != nil { + return nil, err + } + + s.InjectResize() + handleEvent() + + return s, nil +} + +func cleanup() { + os.RemoveAll(tempDir) +} + +func handleEvent() { + screen.Lock() + e := screen.Screen.PollEvent() + screen.Unlock() + if e != nil { + events <- e + } + DoEvent() +} + +func injectKey(key tcell.Key, r rune, mod tcell.ModMask) { + sim.InjectKey(key, r, mod) + handleEvent() +} + +func injectMouse(x, y int, buttons tcell.ButtonMask, mod tcell.ModMask) { + sim.InjectMouse(x, y, buttons, mod) + handleEvent() +} + +func injectString(str string) { + // the tcell simulation screen event channel can only handle + // 10 events at once, so we need to divide up the key events + // into chunks of 10 and handle the 10 events before sending + // another chunk of events + iters := len(str) / 10 + extra := len(str) % 10 + + for i := 0; i < iters; i++ { + s := i * 10 + e := i*10 + 10 + sim.InjectKeyBytes([]byte(str[s:e])) + for i := 0; i < 10; i++ { + handleEvent() + } + } + + sim.InjectKeyBytes([]byte(str[len(str)-extra:])) + for i := 0; i < extra; i++ { + handleEvent() + } +} + +func openFile(file string) { + injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl) + injectString(fmt.Sprintf("open %s", file)) + injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) +} + +func createTestFile(name string, content string) (string, error) { + testf, err := ioutil.TempFile("", name) + if err != nil { + return "", err + } + + if _, err := testf.Write([]byte(content)); err != nil { + return "", err + } + if err := testf.Close(); err != nil { + return "", err + } + + return testf.Name(), nil +} + +func TestMain(m *testing.M) { + var err error + sim, err = startup([]string{}) + if err != nil { + log.Fatalln(err) + os.Exit(1) + } + + retval := m.Run() + cleanup() + + os.Exit(retval) +} + +func TestSimpleEdit(t *testing.T) { + file, err := createTestFile("micro_simple_edit_test", "base content") + if err != nil { + t.Error(err) + return + } + defer os.Remove(file) + + openFile(file) + + var buf *buffer.Buffer + for _, b := range buffer.OpenBuffers { + if b.Path == file { + buf = b + } + } + + if buf == nil { + t.Errorf("Could not find buffer %s", file) + return + } + + injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) + injectKey(tcell.KeyUp, 0, tcell.ModNone) + injectString("first line") + + // test both kinds of backspace + for i := 0; i < len("ne"); i++ { + injectKey(tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone) + } + for i := 0; i < len(" li"); i++ { + injectKey(tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone) + } + injectString("foobar") + + injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) + + data, err := ioutil.ReadFile(file) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, "firstfoobar\nbase content\n", string(data)) +} + +func TestMouse(t *testing.T) { + file, err := createTestFile("micro_mouse_test", "base content") + if err != nil { + t.Error(err) + return + } + defer os.Remove(file) + + openFile(file) + + // buffer: + // base content + // the selections need to happen at different locations to avoid a double click + injectMouse(3, 0, tcell.Button1, tcell.ModNone) + injectKey(tcell.KeyLeft, 0, tcell.ModNone) + injectMouse(0, 0, tcell.ButtonNone, tcell.ModNone) + injectString("secondline") + // buffer: + // secondlinebase content + injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) + // buffer: + // secondline + // base content + injectMouse(2, 0, tcell.Button1, tcell.ModNone) + injectMouse(0, 0, tcell.ButtonNone, tcell.ModNone) + injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) + // buffer: + // + // secondline + // base content + injectKey(tcell.KeyUp, 0, tcell.ModNone) + injectString("firstline") + // buffer: + // firstline + // secondline + // base content + injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) + + data, err := ioutil.ReadFile(file) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data)) +} + +var srTestStart = `foo +foo +foofoofoo +Ernleȝe foo æðelen +` +var srTest2 = `test_string +test_string +test_stringtest_stringtest_string +Ernleȝe test_string æðelen +` +var srTest3 = `test_foo +test_string +test_footest_stringtest_foo +Ernleȝe test_string æðelen +` + +func TestSearchAndReplace(t *testing.T) { + file, err := createTestFile("micro_search_replace_test", srTestStart) + if err != nil { + t.Error(err) + return + } + defer os.Remove(file) + + openFile(file) + + injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl) + injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string")) + injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) + + injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) + + data, err := ioutil.ReadFile(file) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, srTest2, string(data)) + + injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl) + injectString(fmt.Sprintf("replace %s %s", "string", "foo")) + injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) + injectString("ynyny") + injectKey(tcell.KeyEscape, 0, tcell.ModNone) + + injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) + + data, err = ioutil.ReadFile(file) + if err != nil { + t.Error(err) + return + } + + assert.Equal(t, srTest3, string(data)) +} + +func TestMultiCursor(t *testing.T) { + // TODO +} + +func TestSettingsPersistence(t *testing.T) { + // TODO +} + +// more tests (rendering, tabs, plugins)? diff --git a/go.mod b/go.mod index a34c103a..9b6490f9 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d github.com/zyedidia/pty v2.0.0+incompatible // indirect - github.com/zyedidia/tcell v1.4.7 + github.com/zyedidia/tcell v1.4.8 github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415 golang.org/x/text v0.3.2 gopkg.in/sourcemap.v1 v1.0.5 // indirect diff --git a/go.sum b/go.sum index 5f32d621..7dc11ac8 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE= github.com/zyedidia/pty v2.0.0+incompatible h1:Ou5vXL6tvjst+RV8sUFISbuKDnUJPhnpygApMFGweqw= github.com/zyedidia/pty v2.0.0+incompatible/go.mod h1:4y9l9yJZNxRa7GB/fB+mmDmGkG3CqmzLf4vUxGGotEA= -github.com/zyedidia/tcell v1.4.7 h1:bKXRjv8RglPyOFqofzUUJkrdsLs9p9mT89W2ShFFlco= -github.com/zyedidia/tcell v1.4.7/go.mod h1:HhlbMSCcGX15rFDB+Q1Lk3pKEOocsCUAQC3zhZ9sadA= +github.com/zyedidia/tcell v1.4.8 h1:s4zYGOyCNDK4cdrgNVME0SxGizuT/oKY3OyB4Ls2Qpg= +github.com/zyedidia/tcell v1.4.8/go.mod h1:HhlbMSCcGX15rFDB+Q1Lk3pKEOocsCUAQC3zhZ9sadA= github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415 h1:752dTQ5OatJ9M5ULK2+9lor+nzyZz+LYDo3WGngg3Rc= github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415/go.mod h1:8leT8G0Cm8NoJHdrrKHyR9MirWoF4YW7pZh06B6H+1E= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/internal/action/bindings.go b/internal/action/bindings.go index c1999305..fd57eb9c 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -82,7 +82,7 @@ modSearch: case strings.HasPrefix(k, "-"): // We optionally support dashes between modifiers k = k[1:] - case strings.HasPrefix(k, "Ctrl") && k != "Ctrl-h" && k != "CtrlH" && k != "Ctrlh": + case strings.HasPrefix(k, "Ctrl") && k != "CtrlH": // CtrlH technically does not have a 'Ctrl' modifier because it is really backspace k = k[4:] modifiers |= tcell.ModCtrl diff --git a/internal/action/defaults_darwin.go b/internal/action/defaults_darwin.go index 10008135..bc29af26 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -30,8 +30,9 @@ func DefaultBindings() map[string]string { "Alt-{": "ParagraphPrevious", "Alt-}": "ParagraphNext", "Enter": "InsertNewline", - "Ctrl-h": "Backspace", + "CtrlH": "Backspace", "Backspace": "Backspace", + "OldBackspace": "Backspace", "Alt-CtrlH": "DeleteWordLeft", "Alt-Backspace": "DeleteWordLeft", "Tab": "Autocomplete|IndentSelection|InsertTab", diff --git a/internal/action/defaults_other.go b/internal/action/defaults_other.go index bb4e8e7d..d55179f9 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -32,8 +32,9 @@ func DefaultBindings() map[string]string { "Alt-{": "ParagraphPrevious", "Alt-}": "ParagraphNext", "Enter": "InsertNewline", - "Ctrl-h": "Backspace", + "CtrlH": "Backspace", "Backspace": "Backspace", + "OldBackspace": "Backspace", "Alt-CtrlH": "DeleteWordLeft", "Alt-Backspace": "DeleteWordLeft", "Tab": "Autocomplete|IndentSelection|InsertTab", diff --git a/internal/screen/screen.go b/internal/screen/screen.go index c5a80e72..f9661a0c 100644 --- a/internal/screen/screen.go +++ b/internal/screen/screen.go @@ -1,7 +1,7 @@ package screen import ( - "fmt" + "errors" "os" "sync" "unicode" @@ -131,7 +131,7 @@ func TempStart(screenWasNil bool) { } // Init creates and initializes the tcell screen -func Init() { +func Init() error { drawChan = make(chan bool, 8) // Should we enable true color? @@ -151,13 +151,10 @@ func Init() { var err error Screen, err = tcell.NewScreen() if err != nil { - fmt.Println(err) - fmt.Println("Fatal: Micro could not initialize a Screen.") - os.Exit(1) + return err } if err = Screen.Init(); err != nil { - fmt.Println(err) - os.Exit(1) + return err } // restore TERM @@ -168,4 +165,30 @@ func Init() { if config.GetGlobalOption("mouse").(bool) { Screen.EnableMouse() } + + return nil +} + +// InitSimScreen initializes a simulation screen for testing purposes +func InitSimScreen() (tcell.SimulationScreen, error) { + drawChan = make(chan bool, 8) + + // Initilize tcell + var err error + s := tcell.NewSimulationScreen("") + if s == nil { + return nil, errors.New("Failed to get a simulation screen") + } + if err = s.Init(); err != nil { + return nil, err + } + + s.SetSize(80, 24) + Screen = s + + if config.GetGlobalOption("mouse").(bool) { + Screen.EnableMouse() + } + + return s, nil }