cli/clicore: Access all config via the Config interface.

The cli package has been changed accordingly.
This commit is contained in:
Qi Xiao 2019-07-10 21:42:11 +01:00
parent 2584bd2510
commit 123aeae493
5 changed files with 176 additions and 112 deletions

View File

@ -2,6 +2,7 @@ package cli
import (
"os"
"sync"
"github.com/elves/elvish/cli/clicore"
"github.com/elves/elvish/cli/clitypes"
@ -22,8 +23,11 @@ type App struct {
Lastcmd *lastcmd.Mode
}
// AppConfig is a struct containing configurations for initializing an App.
// AppConfig is a struct containing configurations for initializing an App. It
// must not be copied once used.
type AppConfig struct {
Mutex sync.Mutex
MaxHeight int
BeforeReadline []func()
@ -63,30 +67,9 @@ func NewApp(cfg *AppConfig, t clicore.TTY, sigs clicore.SignalSource) *App {
core: coreApp,
cfg: cfg,
}
coreApp.Config = &coreConfig{
maxHeight: cfg.MaxHeight,
rpromptPersistent: cfg.RPromptPersistent,
}
coreApp.BeforeReadline = cfg.BeforeReadline
recordCmd := func(code string) {
if cfg.HistoryStore == nil {
return
}
_, err := cfg.HistoryStore.AddCmd(histutil.Entry{Text: code})
if err != nil {
coreApp.Notify("db error: " + err.Error())
}
}
afterReadline := append([]func(string){recordCmd}, cfg.AfterReadline...)
coreApp.AfterReadline = afterReadline
coreApp.Highlighter = cfg.Highlighter
coreApp.Prompt = cfg.Prompt
coreApp.RPrompt = cfg.RPrompt
insertMode := newInsertMode(&cfg.InsertModeConfig, app)
app.Insert = insertMode
coreApp.InitMode = insertMode
coreApp.Config = coreConfig{app}
app.Insert = newInsertMode(&cfg.InsertModeConfig, app)
lsMode := &listing.Mode{}
app.Listing = lsMode
app.Histlist = &histlist.Mode{

View File

@ -22,29 +22,8 @@ type App struct {
state clitypes.State
// Configuration that can be modified concurrently.
// Configuration.
Config Config
// Functions called when ReadCode starts.
BeforeReadline []func()
// Functions called when ReadCode ends; the argument is the code that has
// just been read.
AfterReadline []func(string)
// Code highlighter.
Highlighter Highlighter
// Left-hand and right-hand prompt.
Prompt, RPrompt Prompt
// Initial mode.
InitMode clitypes.Mode
}
// Config is the configuration for an App.
type Config interface {
MaxHeight() int
RPromptPersistent() bool
}
// NewApp creates a new App from two abstract dependencies. The creation does
@ -52,7 +31,7 @@ type Config interface {
// active. This is the most general way to create an App.
func NewApp(t TTY, sigs SignalSource) *App {
lp := newLoop()
app := &App{loop: lp, tty: t, sigs: sigs}
app := &App{loop: lp, tty: t, sigs: sigs, Config: DefaultConfig{}}
lp.HandleCb(app.handle)
lp.RedrawCb(app.redraw)
return app
@ -98,7 +77,7 @@ func (app *App) handle(e event) handleResult {
}
return handleResult{}
case term.Event:
action := getMode(app.state.Mode(), app.InitMode).HandleEvent(e, &app.state)
action := getMode(app.state.Mode(), app.Config.InitMode()).HandleEvent(e, &app.state)
switch action {
case clitypes.CommitCode:
@ -114,11 +93,13 @@ func (app *App) handle(e event) handleResult {
}
func (app *App) triggerPrompts(force bool) {
if app.Prompt != nil {
app.Prompt.Trigger(force)
prompt := app.Config.Prompt()
rprompt := app.Config.RPrompt()
if prompt != nil {
prompt.Trigger(force)
}
if app.RPrompt != nil {
app.RPrompt.Trigger(force)
if rprompt != nil {
rprompt.Trigger(force)
}
}
@ -145,16 +126,16 @@ func (app *App) redraw(flag redrawFlag) {
// Prepare the code: applying pending, and highlight.
code, dot := applyPending(rawState)
styledCode, errors := highlighterGet(app.Highlighter, code)
styledCode, errors := highlighterGet(app.Config.Highlighter(), code)
// TODO: Apply transformerForPending to pending code.
// Render onto buffers.
setup := &renderSetup{
height, width,
promptGet(app.Prompt), promptGet(app.RPrompt),
promptGet(app.Config.Prompt()), promptGet(app.Config.RPrompt()),
styledCode, dot, errors,
rawState.Notes,
getMode(rawState.Mode, app.InitMode)}
getMode(rawState.Mode, app.Config.InitMode())}
bufNotes, bufMain := render(setup)
@ -238,14 +219,17 @@ func (app *App) ReadCode() (string, error) {
}
}()
}
if app.Prompt != nil {
relayLateUpdates(app.Prompt.LateUpdates())
prompt := app.Config.Prompt()
if prompt != nil {
relayLateUpdates(prompt.LateUpdates())
}
if app.RPrompt != nil {
relayLateUpdates(app.RPrompt.LateUpdates())
rprompt := app.Config.RPrompt()
if rprompt != nil {
relayLateUpdates(rprompt.LateUpdates())
}
if app.Highlighter != nil {
relayLateUpdates(app.Highlighter.LateUpdates())
highlighter := app.Config.Highlighter()
if highlighter != nil {
relayLateUpdates(highlighter.LateUpdates())
}
// Trigger an initial prompt update.
@ -255,16 +239,10 @@ func (app *App) ReadCode() (string, error) {
defer app.state.Reset()
// BeforeReadline and AfterReadline hooks.
for _, f := range app.BeforeReadline {
f()
}
if len(app.AfterReadline) > 0 {
defer func() {
for _, f := range app.AfterReadline {
f(app.state.Code())
}
}()
}
app.Config.BeforeReadline()
defer func() {
app.Config.AfterReadline(app.state.Code())
}()
return app.loop.Run()
}
@ -293,13 +271,3 @@ func (app *App) Notify(note string) {
app.state.AddNote(note)
app.Redraw(false)
}
// AddBeforeReadline adds a new before-readline hook function.
func (app *App) AddBeforeReadline(f func()) {
app.BeforeReadline = append(app.BeforeReadline, f)
}
// AddAfterReadline adds a new after-readline hook function.
func (app *App) AddAfterReadline(f func(string)) {
app.AfterReadline = append(app.AfterReadline, f)
}

View File

@ -9,12 +9,42 @@ import (
"testing"
"time"
clitypes "github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/cli/term"
"github.com/elves/elvish/edit/ui"
"github.com/elves/elvish/styled"
"github.com/elves/elvish/sys"
)
type testConfig struct {
maxHeight int
rpromptPersistent bool
beforeReadline func()
afterReadline func(string)
highlighter Highlighter
prompt Prompt
rprompt Prompt
}
func (tc testConfig) MaxHeight() int { return tc.maxHeight }
func (tc testConfig) RPromptPersistent() bool { return tc.rpromptPersistent }
func (tc testConfig) Highlighter() Highlighter { return tc.highlighter }
func (tc testConfig) Prompt() Prompt { return tc.prompt }
func (tc testConfig) RPrompt() Prompt { return tc.rprompt }
func (tc testConfig) InitMode() clitypes.Mode { return nil }
func (tc testConfig) BeforeReadline() {
if tc.beforeReadline != nil {
tc.beforeReadline()
}
}
func (tc testConfig) AfterReadline(s string) {
if tc.afterReadline != nil {
tc.afterReadline(s)
}
}
func TestReadCode_AbortsOnSetupError(t *testing.T) {
ed, tty, _ := setup()
@ -78,7 +108,7 @@ func TestReadCode_CallsBeforeReadlineOnce(t *testing.T) {
ed, tty, _ := setup()
called := 0
ed.AddBeforeReadline(func() { called++ })
ed.Config = testConfig{beforeReadline: func() { called++ }}
// Causes BasicMode to quit
tty.Inject(term.KeyEvent{Rune: '\n'})
@ -94,10 +124,10 @@ func TestReadCode_CallsAfterReadlineOnceWithCode(t *testing.T) {
called := 0
code := ""
ed.AddAfterReadline(func(s string) {
ed.Config = testConfig{afterReadline: func(s string) {
called++
code = s
})
}}
// Causes BasicMode to write state.Code and then quit
tty.Inject(term.KeyEvent{Rune: 'a'})
tty.Inject(term.KeyEvent{Rune: 'b'})
@ -114,18 +144,10 @@ func TestReadCode_CallsAfterReadlineOnceWithCode(t *testing.T) {
}
}
type testConfig struct {
maxHeight int
rpromptPersistent bool
}
func (tc testConfig) MaxHeight() int { return tc.maxHeight }
func (tc testConfig) RPromptPersistent() bool { return tc.rpromptPersistent }
func TestReadCode_RespectsMaxHeight(t *testing.T) {
ed, tty, _ := setup()
ed.Config = &testConfig{maxHeight: 2}
ed.Config = testConfig{maxHeight: 2}
tty.SetSize(10, 5) // Width = 5 to make it easy to test
// The code needs 3 lines to completely show.
@ -147,12 +169,12 @@ var bufChTimeout = 1 * time.Second
func TestReadCode_RendersHighlightedCode(t *testing.T) {
ed, tty, _ := setup()
ed.Highlighter = fakeHighlighter{
ed.Config = testConfig{highlighter: fakeHighlighter{
get: func(code string) (styled.Text, []error) {
return styled.Text{
&styled.Segment{styled.Style{Foreground: "red"}, code}}, nil
},
}
}}
tty.Inject(term.KeyEvent{Rune: 'a'})
tty.Inject(term.KeyEvent{Rune: 'b'})
tty.Inject(term.KeyEvent{Rune: 'c'})
@ -180,7 +202,7 @@ func TestReadCode_RedrawsOnHighlighterLateUpdate(t *testing.T) {
func TestReadCode_RendersPrompt(t *testing.T) {
ed, tty, _ := setup()
ed.Prompt = constPrompt{styled.Plain("> ")}
ed.Config = testConfig{prompt: constPrompt{styled.Plain("> ")}}
tty.Inject(term.KeyEvent{Rune: 'a'})
codeCh, _ := ed.readCodeAsync()
@ -199,7 +221,7 @@ func TestReadCode_RendersRPrompt(t *testing.T) {
ed, tty, _ := setup()
tty.SetSize(80, 4) // Set a width of 4 for easier testing.
ed.RPrompt = constPrompt{styled.Plain("R")}
ed.Config = testConfig{rprompt: constPrompt{styled.Plain("R")}}
tty.Inject(term.KeyEvent{Rune: 'a'})
codeCh, _ := ed.readCodeAsync()
@ -217,7 +239,7 @@ func TestReadCode_TriggersPrompt(t *testing.T) {
ed, tty, _ := setup()
called := 0
ed.Prompt = fakePrompt{trigger: func(bool) { called++ }}
ed.Config = testConfig{prompt: fakePrompt{trigger: func(bool) { called++ }}}
codeCh, _ := ed.readCodeAsync()
cleanup(tty, codeCh)
@ -235,7 +257,7 @@ func TestReadCode_RedrawsOnPromptLateUpdate(t *testing.T) {
get: func() styled.Text { return styled.Plain(promptContent) },
lateUpdates: make(chan styled.Text),
}
ed.Prompt = prompt
ed.Config = testConfig{prompt: prompt}
codeCh, _ := ed.readCodeAsync()
bufOldPrompt := ui.NewBufferBuilder(80).

44
cli/clicore/config.go Normal file
View File

@ -0,0 +1,44 @@
package clicore
import clitypes "github.com/elves/elvish/cli/clitypes"
// Config is the configuration for an App.
type Config interface {
MaxHeight() int
RPromptPersistent() bool
BeforeReadline()
AfterReadline(string)
Highlighter() Highlighter
Prompt() Prompt
RPrompt() Prompt
InitMode() clitypes.Mode
}
// DefaultConfig implements the Config interface, providing sensible default
// behavior. Other implementations of Config cam embed this struct and only
// implement the methods that it needs.
type DefaultConfig struct{}
// MaxHeight returns -1.
func (DefaultConfig) MaxHeight() int { return -1 }
// RPromptPersistent returns false.
func (DefaultConfig) RPromptPersistent() bool { return false }
// BeforeReadline does nothing.
func (DefaultConfig) BeforeReadline() {}
// AfterReadline does nothing.
func (DefaultConfig) AfterReadline(string) {}
// Highlighter returns nil.
func (DefaultConfig) Highlighter() Highlighter { return nil }
// Prompt returns nil.
func (DefaultConfig) Prompt() Prompt { return nil }
// RPrompt returns nil.
func (DefaultConfig) RPrompt() Prompt { return nil }
// InitMode returns nil.
func (DefaultConfig) InitMode() clitypes.Mode { return nil }

View File

@ -1,22 +1,69 @@
package cli
import "sync"
import (
"github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/cli/histutil"
)
// Adaps App to implement the clicore.Config interface.
type coreConfig struct {
mu sync.Mutex
maxHeight int
rpromptPersistent bool
*App
}
func (cc *coreConfig) MaxHeight() int {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.maxHeight
func (cc coreConfig) MaxHeight() (ret int) {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
return cc.cfg.MaxHeight
}
func (cc *coreConfig) RPromptPersistent() bool {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.rpromptPersistent
func (cc coreConfig) RPromptPersistent() bool {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
return cc.cfg.RPromptPersistent
}
func (cc coreConfig) BeforeReadline() {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
for _, f := range cc.cfg.BeforeReadline {
f()
}
}
func (cc coreConfig) AfterReadline(code string) {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
for _, f := range cc.cfg.AfterReadline {
f(code)
}
if cc.cfg.HistoryStore != nil {
_, err := cc.cfg.HistoryStore.AddCmd(histutil.Entry{Text: code})
if err != nil {
cc.core.Notify("db error: " + err.Error())
}
}
}
func (cc coreConfig) Highlighter() Highlighter {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
return cc.cfg.Highlighter
}
func (cc coreConfig) Prompt() Prompt {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
return cc.cfg.Prompt
}
func (cc coreConfig) RPrompt() Prompt {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
return cc.cfg.RPrompt
}
func (cc coreConfig) InitMode() clitypes.Mode {
cc.cfg.Mutex.Lock()
defer cc.cfg.Mutex.Unlock()
return cc.Insert
}