Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/micro/initlua.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ func luaImportMicro() *lua.LTable {
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() *action.BufPane {
return action.MainTab().CurPane()
t := action.MainTab()
if t == nil {
return nil
}
return t.CurPane()
}))
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab))
ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList {
Expand Down
53 changes: 53 additions & 0 deletions cmd/micro/initlua_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"testing"

"github.com/micro-editor/micro/v2/internal/action"
ulua "github.com/micro-editor/micro/v2/internal/lua"
"github.com/stretchr/testify/assert"
lua "github.com/yuin/gopher-lua"
)

// callMicroFn calls micro.<name>() and returns its single result.
func callMicroFn(t *testing.T, pkg *lua.LTable, name string) lua.LValue {
t.Helper()

err := ulua.L.CallByParam(lua.P{
Fn: ulua.L.GetField(pkg, name),
NRet: 1,
Protect: true,
})
if err != nil {
t.Fatalf("micro.%s(): %v", name, err)
}

got := ulua.L.Get(-1)
ulua.L.Pop(1)
return got
}

// Plugin hooks can run before InitTabs, so these wrappers must return nil
// instead of dereferencing the nil tab that MainTab() returns.
func TestLuaCurrentPaneAndTab(t *testing.T) {
savedTabs := action.Tabs
t.Cleanup(func() { action.Tabs = savedTabs })

for _, tc := range []struct {
name string
tabs *action.TabList
wantNil bool
}{
// TestMain has already run startup, so savedTabs is a live tab list.
{"initialized", savedTabs, false},
{"uninitialized", nil, true},
{"empty", &action.TabList{}, true},
} {
t.Run(tc.name, func(t *testing.T) {
action.Tabs = tc.tabs
pkg := luaImportMicro()
assert.Equal(t, tc.wantNil, callMicroFn(t, pkg, "CurPane") == lua.LNil)
assert.Equal(t, tc.wantNil, callMicroFn(t, pkg, "CurTab") == lua.LNil)
})
}
}
89 changes: 51 additions & 38 deletions cmd/micro/micro.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,56 @@ func exit(rc int) {
os.Exit(rc)
}

// initEditor runs the startup sequence shared by main and the tests, in the
// order the editor depends on: globals exist before any buffer is loaded, and
// tabs exist before plugins run init.
//
// - onErr reports a failure the editor survives; startup continues past it.
// - A returned error means startup cannot continue.
// - The returned buffers are empty when there is nothing to open.
func initEditor(args []string, onErr func(error)) ([]*buffer.Buffer, error) {
if err := config.LoadAllPlugins(); err != nil {
onErr(err)
}

if err := checkBackup("bindings.json"); err != nil {
return nil, err
}

action.InitBindings()
action.InitCommands()

timerChan = make(chan func())

if err := config.RunPluginFn("preinit"); err != nil {
onErr(err)
}

action.InitGlobals()
buffer.SetMessager(action.InfoBar)

b := LoadInput(args)
if len(b) == 0 {
return b, nil
}

action.InitTabs(b)

if err := config.RunPluginFn("init"); err != nil {
onErr(err)
}

if err := config.RunPluginFn("postinit"); err != nil {
onErr(err)
}

if err := config.InitColorscheme(); err != nil {
onErr(err)
}

return b, nil
}

func main() {
defer func() {
if util.Stdout.Len() > 0 {
Expand Down Expand Up @@ -405,55 +455,18 @@ func main() {
}
}()

err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
}

err = checkBackup("bindings.json")
b, err := initEditor(flag.Args(), func(err error) { screen.TermMessage(err) })
if err != nil {
screen.TermMessage(err)
exit(1)
}

action.InitBindings()
action.InitCommands()

timerChan = make(chan func())

err = config.RunPluginFn("preinit")
if err != nil {
screen.TermMessage(err)
}

action.InitGlobals()
buffer.SetMessager(action.InfoBar)
args := flag.Args()
b := LoadInput(args)

if len(b) == 0 {
// No buffers to open
screen.Screen.Fini()
runtime.Goexit()
}

action.InitTabs(b)

err = config.RunPluginFn("init")
if err != nil {
screen.TermMessage(err)
}

err = config.RunPluginFn("postinit")
if err != nil {
screen.TermMessage(err)
}

err = config.InitColorscheme()
if err != nil {
screen.TermMessage(err)
}

if clipErr != nil {
log.Println(clipErr, " or change 'clipboard' option")
}
Expand Down
31 changes: 11 additions & 20 deletions cmd/micro/micro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"fmt"
"log"
"os"
"strings"
"testing"

"github.com/go-errors/errors"
"github.com/micro-editor/micro/v2/internal/action"
"github.com/micro-editor/micro/v2/internal/buffer"
"github.com/micro-editor/micro/v2/internal/config"
"github.com/micro-editor/micro/v2/internal/screen"
Expand Down Expand Up @@ -67,33 +67,24 @@ func startup(args []string) (tcell.SimulationScreen, error) {
}
}()

err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
}

action.InitBindings()
action.InitCommands()

err = config.InitColorscheme()
// The editor warns and carries on when plugins or the colorscheme fail to
// load; a test should report every one of them and stop.
var startupErrs []string
b, err := initEditor(args, func(reported error) {
// Strings, not errors.Join: go.mod and CI still support Go 1.19.
startupErrs = append(startupErrs, reported.Error())
})
if err != nil {
return nil, err
}

b := LoadInput(args)
if len(startupErrs) > 0 {
return nil, errors.New(strings.Join(startupErrs, "\n"))
}

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()

Expand Down
14 changes: 12 additions & 2 deletions internal/action/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
"github.com/micro-editor/micro/v2/internal/util"
)

// ErrNoPane is returned when an operation needs the current pane and there is none.
var ErrNoPane = errors.New("No pane open")

// A Command contains information about how to execute a command
// It has the action for that command as well as a completer function
type Command struct {
Expand Down Expand Up @@ -588,7 +591,10 @@ func doSetGlobalOptionNative(option string, nativeValue any) error {
b.UpdateRules()
}
} else if option == "infobar" || option == "keymenu" || option == "tabalways" {
Tabs.Resize()
// InitTabs reads the new value itself if a plugin sets it before then.
if Tabs != nil {
Tabs.Resize()
}
} else if option == "mouse" {
if !nativeValue.(bool) {
screen.Screen.DisableMouse()
Expand Down Expand Up @@ -639,7 +645,11 @@ func SetGlobalOptionNative(option string, nativeValue any, writeToFile bool) err
// check for local option first...
for _, s := range config.LocalSettings {
if s == option {
return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue)
t := MainTab()
if t == nil {
return ErrNoPane
}
return t.CurPane().Buf.SetOptionNative(option, nativeValue)
}
}

Expand Down
27 changes: 27 additions & 0 deletions internal/action/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package action

import (
"testing"

"github.com/micro-editor/micro/v2/internal/config"
"github.com/stretchr/testify/assert"
)

// Plugins can set options from preinit or onBufferOpen, before Tabs exists.
func TestSetGlobalOptionBeforeTabs(t *testing.T) {
config.InitRuntimeFiles(false)
if err := config.InitGlobalSettings(); err != nil {
t.Fatal(err)
}

saved := Tabs
defer func() { Tabs = saved }()
Tabs = nil

// Local options are set on the current pane's buffer, and there is none.
assert.Equal(t, ErrNoPane, SetGlobalOptionPlug("filetype", "go"))

// Layout options resize the tabs, which do not exist yet.
assert.NoError(t, SetGlobalOptionPlug("tabalways", "true"))
assert.Equal(t, true, config.GlobalSettings["tabalways"])
}
4 changes: 4 additions & 0 deletions internal/action/tab.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ func InitTabs(bufs []*buffer.Buffer) {
}

func MainTab() *Tab {
// Tabs is nil until InitTabs finishes; plugin hooks can fire before then.
if Tabs == nil || len(Tabs.List) == 0 {
return nil
}
return Tabs.List[Tabs.Active()]
}

Expand Down
19 changes: 19 additions & 0 deletions internal/action/tab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package action

import (
"testing"

"github.com/stretchr/testify/assert"
)

// MainTab must return nil, not panic, before Tabs is initialized.
func TestMainTabHandlesUninitializedTabs(t *testing.T) {
saved := Tabs
defer func() { Tabs = saved }()

Tabs = nil
assert.Nil(t, MainTab(), "MainTab() with nil Tabs")

Tabs = &TabList{}
assert.Nil(t, MainTab(), "MainTab() with empty Tabs.List")
}
7 changes: 6 additions & 1 deletion internal/buffer/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1468,8 +1468,13 @@ func (b *Buffer) SearchMatch(pos Loc) bool {
return b.LineArray.SearchMatch(b, pos)
}

// WriteLog writes a string to the log buffer
// WriteLog writes a string to the log buffer.
// Plugins can reach this before the log buffer exists, from preinit or from
// the log buffer's own onBufferOpen.
func WriteLog(s string) {
if LogBuf == nil {
return
}
LogBuf.EventHandler.Insert(LogBuf.End(), s)
}

Expand Down
16 changes: 16 additions & 0 deletions internal/buffer/buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,22 @@ func benchEdit(testingB *testing.B, nLines, nCursors int) {
b.Close()
}

// Plugins can log from preinit and from the log buffer's own onBufferOpen,
// both of which run before LogBuf is assigned.
func TestWriteLogBeforeLogBufExists(t *testing.T) {
saved := LogBuf
t.Cleanup(func() { LogBuf = saved })

LogBuf = nil
WriteLog("dropped")

LogBuf = NewBufferFromString("", "", BTLog)
t.Cleanup(LogBuf.Close)
WriteLog("kept")

assert.Equal(t, []byte("kept"), LogBuf.Bytes())
}

func BenchmarkCreateAndClose10Lines(b *testing.B) {
benchCreateAndClose(b, 10)
}
Expand Down
Loading