mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-13 09:57:51 +08:00
parent
a46d854ab5
commit
5291dae422
|
@ -22,7 +22,8 @@ insert:binding = (binding-map [
|
|||
&Ctrl-U= $kill-sol~
|
||||
&Ctrl-K= $kill-eol~
|
||||
|
||||
&Alt-,= $lastcmd:start~
|
||||
&Alt-,= $lastcmd:start~
|
||||
&Ctrl-R= $histlist:start~
|
||||
|
||||
&Ctrl-D= $commit-eof~
|
||||
&Default= $insert:default-handler~
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package newedit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/elves/elvish/edit/history/histutil"
|
||||
"github.com/elves/elvish/eval"
|
||||
"github.com/elves/elvish/eval/vars"
|
||||
"github.com/elves/elvish/newedit/core"
|
||||
|
@ -41,11 +43,18 @@ func NewEditor(in, out *os.File, ev *eval.Evaler, st storedefs.Store) *Editor {
|
|||
}).
|
||||
AddGoFns("<edit>", bufferBuiltins(ed.State()))
|
||||
|
||||
// Add the builtin hook of appending history in after-readline.
|
||||
ed.AddAfterReadline(func(code string) {
|
||||
st.AddCmd(code)
|
||||
// TODO: Log errors
|
||||
})
|
||||
histFuser, err := histutil.NewFuser(st)
|
||||
if err == nil {
|
||||
// Add the builtin hook of appending history in after-readline.
|
||||
ed.AddAfterReadline(func(code string) {
|
||||
err := histFuser.AddCmd(code)
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, "failed to add command to history")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintln(out, "failed to initialize history facilities")
|
||||
}
|
||||
|
||||
// Elvish hook APIs
|
||||
var beforeReadline func()
|
||||
|
@ -67,9 +76,13 @@ func NewEditor(in, out *os.File, ev *eval.Evaler, st storedefs.Store) *Editor {
|
|||
// Listing modes.
|
||||
lsMode, lsBinding, lsNs := initListing(ed)
|
||||
ns.AddNs("listing", lsNs)
|
||||
|
||||
lastcmdNs := initLastcmd(ed, ev, st, lsMode, lsBinding)
|
||||
ns.AddNs("lastcmd", lastcmdNs)
|
||||
|
||||
histlistNs := initHistlist(ed, ev, histFuser.AllCmds, lsMode, lsBinding)
|
||||
ns.AddNs("histlist", histlistNs)
|
||||
|
||||
// Evaluate default bindings.
|
||||
evalDefaultBinding(ev, ns)
|
||||
|
||||
|
|
81
newedit/histlist/histlist.go
Normal file
81
newedit/histlist/histlist.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package histlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/elves/elvish/edit/ui"
|
||||
"github.com/elves/elvish/newedit/listing"
|
||||
"github.com/elves/elvish/newedit/types"
|
||||
"github.com/elves/elvish/styled"
|
||||
)
|
||||
|
||||
// Mode represents the histlist mode. It implements the types.Mode interface by
|
||||
// embedding a *listing.Mode.
|
||||
type Mode struct {
|
||||
*listing.Mode
|
||||
KeyHandler func(ui.Key) types.HandlerAction
|
||||
}
|
||||
|
||||
// Start starts the histlist mode.
|
||||
func (m *Mode) Start(cmds []string) {
|
||||
m.Mode.Start(listing.StartConfig{
|
||||
Name: "HISTLIST",
|
||||
KeyHandler: m.KeyHandler,
|
||||
ItemsGetter: func(p string) listing.Items {
|
||||
return getEntries(cmds, p)
|
||||
},
|
||||
StartFilter: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Given all commands, and a pattern, returning all matching entries.
|
||||
func getEntries(cmds []string, p string) items {
|
||||
// TODO: Show the real in-storage IDs of cmds, not their in-memory indicies.
|
||||
var entries []entry
|
||||
for i, line := range cmds {
|
||||
if strings.Contains(line, p) {
|
||||
entries = append(entries, entry{line, i})
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// A slice of entries, implementing the listing.Items interface.
|
||||
type items []entry
|
||||
|
||||
// An entry to show, which is just a line plus its index.
|
||||
type entry struct {
|
||||
content string
|
||||
index int
|
||||
}
|
||||
|
||||
func (it items) Len() int {
|
||||
return len(it)
|
||||
}
|
||||
|
||||
func (it items) Show(i int) styled.Text {
|
||||
// TODO: The alignment of the index works up to 10000 entries.
|
||||
return styled.Unstyled(fmt.Sprintf("%4d %s", it[i].index+1, it[i].content))
|
||||
}
|
||||
|
||||
func (it items) Accept(i int, st *types.State) {
|
||||
st.Mutex.Lock()
|
||||
defer st.Mutex.Unlock()
|
||||
raw := &st.Raw
|
||||
|
||||
if raw.Code == "" {
|
||||
insertAtDot(raw, it[i].content)
|
||||
} else {
|
||||
// TODO: This works well when the cursor is at the end, but can be
|
||||
// unexpected when the cursor is in the middle.
|
||||
insertAtDot(raw, "\n"+it[i].content)
|
||||
}
|
||||
}
|
||||
|
||||
func insertAtDot(raw *types.RawState, text string) {
|
||||
// NOTE: This is an duplicate with (*types.State).InsertAtDot, without any
|
||||
// locks because we accept RawState.
|
||||
raw.Code = raw.Code[:raw.Dot] + text + raw.Code[raw.Dot:]
|
||||
raw.Dot += len(text)
|
||||
}
|
57
newedit/histlist/histlist_test.go
Normal file
57
newedit/histlist/histlist_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package histlist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/elves/elvish/newedit/listing"
|
||||
"github.com/elves/elvish/newedit/types"
|
||||
"github.com/elves/elvish/styled"
|
||||
"github.com/elves/elvish/tt"
|
||||
)
|
||||
|
||||
var Args = tt.Args
|
||||
|
||||
var testCmds = []string{}
|
||||
|
||||
func TestGetEntries(t *testing.T) {
|
||||
cmds := []string{
|
||||
"put 1",
|
||||
"echo 2",
|
||||
"print 3",
|
||||
"repr 4",
|
||||
}
|
||||
|
||||
tt.Test(t, tt.Fn("getEntries", getEntries), tt.Table{
|
||||
// Show all commands.
|
||||
Args(cmds, "").Rets(listing.MatchItems(
|
||||
styled.Unstyled(" 1 put 1"),
|
||||
styled.Unstyled(" 2 echo 2"),
|
||||
styled.Unstyled(" 3 print 3"),
|
||||
styled.Unstyled(" 4 repr 4"),
|
||||
)),
|
||||
// Filter.
|
||||
Args(cmds, "pr").Rets(listing.MatchItems(
|
||||
styled.Unstyled(" 3 print 3"),
|
||||
styled.Unstyled(" 4 repr 4"),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccept(t *testing.T) {
|
||||
cmds := []string{
|
||||
"put 1",
|
||||
"echo 2",
|
||||
}
|
||||
entries := getEntries(cmds, "")
|
||||
st := types.State{}
|
||||
|
||||
entries.Accept(0, &st)
|
||||
if st.Code() != "put 1" {
|
||||
t.Errorf("Accept doesn't insert command")
|
||||
}
|
||||
|
||||
entries.Accept(1, &st)
|
||||
if st.Code() != "put 1\necho 2" {
|
||||
t.Errorf("Accept doesn't insert command with newline")
|
||||
}
|
||||
}
|
30
newedit/histlist_api.go
Normal file
30
newedit/histlist_api.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package newedit
|
||||
|
||||
import (
|
||||
"github.com/elves/elvish/eval"
|
||||
"github.com/elves/elvish/newedit/histlist"
|
||||
"github.com/elves/elvish/newedit/listing"
|
||||
)
|
||||
|
||||
// Initializes states for the histlist mode and its API.
|
||||
func initHistlist(ed editor, ev *eval.Evaler, getCmds func() ([]string, error), lsMode *listing.Mode, lsBinding *bindingMap) eval.Ns {
|
||||
binding := emptyBindingMap
|
||||
mode := histlist.Mode{
|
||||
Mode: lsMode,
|
||||
KeyHandler: keyHandlerFromBindings(ed, ev, &binding, lsBinding),
|
||||
}
|
||||
ns := eval.Ns{}.
|
||||
AddGoFn("<edit:histlist>", "start", func() {
|
||||
startHistlist(ed, getCmds, &mode)
|
||||
})
|
||||
return ns
|
||||
}
|
||||
|
||||
func startHistlist(ed editor, getCmds func() ([]string, error), mode *histlist.Mode) {
|
||||
cmds, err := getCmds()
|
||||
if err != nil {
|
||||
ed.Notify("db error: " + err.Error())
|
||||
}
|
||||
mode.Start(cmds)
|
||||
ed.State().SetMode(mode)
|
||||
}
|
44
newedit/histlist_api_test.go
Normal file
44
newedit/histlist_api_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package newedit
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/elves/elvish/edit/ui"
|
||||
|
||||
"github.com/elves/elvish/edit/history/histutil"
|
||||
"github.com/elves/elvish/eval"
|
||||
"github.com/elves/elvish/newedit/listing"
|
||||
"github.com/elves/elvish/newedit/types"
|
||||
)
|
||||
|
||||
func TestHistlist_Start(t *testing.T) {
|
||||
ed := &fakeEditor{}
|
||||
ev := eval.NewEvaler()
|
||||
lsMode := listing.Mode{}
|
||||
lsBinding := emptyBindingMap
|
||||
// TODO: Move this into common setup.
|
||||
histFuser, err := histutil.NewFuser(testStore)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ns := initHistlist(ed, ev, histFuser.AllCmds, &lsMode, &lsBinding)
|
||||
|
||||
// Call <edit:histlist>:start.
|
||||
fm := eval.NewTopFrame(ev, eval.NewInternalSource("[test]"), nil)
|
||||
fm.Call(getFn(ns, "start"), eval.NoArgs, eval.NoOpts)
|
||||
|
||||
// Verify that the current mode supports listing.
|
||||
lister, ok := ed.state.Mode().(types.Lister)
|
||||
if !ok {
|
||||
t.Errorf("Mode is not Lister after <edit:histlist>:start")
|
||||
}
|
||||
// Verify the actual listing.
|
||||
buf := ui.Render(lister.List(10), 30)
|
||||
wantBuf := ui.NewBufferBuilder(30).
|
||||
WriteString(" 1 echo hello world", "7").Buffer()
|
||||
if !reflect.DeepEqual(buf, wantBuf) {
|
||||
t.Errorf("Rendered listing is %v, want %v", buf, wantBuf)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ func TestInitLastCmd_Start(t *testing.T) {
|
|||
// Verify that the current mode supports listing.
|
||||
lister, ok := ed.state.Mode().(types.Lister)
|
||||
if !ok {
|
||||
t.Errorf("Mode is not Lister after <edit:listing>:start")
|
||||
t.Errorf("Mode is not Lister after <edit:lastcmd>:start")
|
||||
}
|
||||
// Verify the listing.
|
||||
buf := ui.Render(lister.List(10), 20)
|
||||
|
|
|
@ -38,6 +38,7 @@ type StartConfig struct {
|
|||
ItemsGetter func(filter string) Items
|
||||
StartFilter bool
|
||||
AutoAccept bool
|
||||
SelectLast bool
|
||||
}
|
||||
|
||||
// Items is an interface for accessing items to show in the listing mode.
|
||||
|
@ -62,7 +63,8 @@ func (m *Mode) Start(cfg StartConfig) {
|
|||
*m = Mode{
|
||||
StartConfig: cfg,
|
||||
state: State{
|
||||
filtering: cfg.StartFilter, itemsGetter: cfg.ItemsGetter},
|
||||
itemsGetter: cfg.ItemsGetter, selectLast: cfg.SelectLast,
|
||||
filtering: cfg.StartFilter},
|
||||
}
|
||||
m.state.refilter("")
|
||||
}
|
||||
|
|
|
@ -53,6 +53,16 @@ func TestModeRenderFlag(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStart_SelectLast(t *testing.T) {
|
||||
m := Mode{}
|
||||
m.Start(StartConfig{ItemsGetter: func(string) Items {
|
||||
return fakeItems{10}
|
||||
}, SelectLast: true})
|
||||
if m.state.selected != 9 {
|
||||
t.Errorf("SelectLast did not cause the last item to be selected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleEvent_CallsKeyHandler(t *testing.T) {
|
||||
m := Mode{}
|
||||
key := ui.K('a')
|
||||
|
|
|
@ -3,11 +3,13 @@ package listing
|
|||
// State keeps the state of the listing mode.
|
||||
type State struct {
|
||||
itemsGetter func(string) Items
|
||||
filtering bool
|
||||
filter string
|
||||
items Items
|
||||
first int
|
||||
selected int
|
||||
selectLast bool
|
||||
|
||||
filtering bool
|
||||
filter string
|
||||
items Items
|
||||
first int
|
||||
selected int
|
||||
}
|
||||
|
||||
func (st *State) refilter(f string) {
|
||||
|
@ -17,6 +19,11 @@ func (st *State) refilter(f string) {
|
|||
} else {
|
||||
st.items = st.itemsGetter(f)
|
||||
}
|
||||
if st.selectLast {
|
||||
st.selected = st.items.Len() - 1
|
||||
} else {
|
||||
st.selected = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Up moves the selection up.
|
||||
|
|
Loading…
Reference in New Issue
Block a user