extract Selection object to its own package

This commit is contained in:
Daisuke Maki 2026-02-20 14:56:29 +09:00
parent 37a9b23c61
commit bdf9b33e6d
8 changed files with 138 additions and 141 deletions

View file

@ -11,12 +11,12 @@ import (
"context"
"github.com/google/btree"
"github.com/lestrrat-go/pdebug"
"github.com/peco/peco/hub"
"github.com/peco/peco/internal/keyseq"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/peco/peco/selection"
)
// Action describes an action that can be executed upon receiving user input.
@ -196,7 +196,7 @@ func init() {
}
// selectLine marks the line as dirty and adds it to the selection.
func selectLine(l line.Line, s *Selection) {
func selectLine(l line.Line, s *selection.Set) {
l.SetDirty(true)
s.Add(l)
}
@ -368,7 +368,7 @@ func doFinish(ctx context.Context, state *Peco, _ Event) {
return
}
sel := NewSelection()
sel := selection.New()
state.Selection().Copy(sel)
if sel.Len() == 0 {
if l, err := state.CurrentLineBuffer().LineAt(state.Location().LineNumber()); err == nil {
@ -377,12 +377,8 @@ func doFinish(ctx context.Context, state *Peco, _ Event) {
}
var stdin bytes.Buffer
sel.Ascend(func(it btree.Item) bool {
line, ok := it.(line.Line)
if !ok {
return true
}
stdin.WriteString(line.Buffer())
sel.Ascend(func(l line.Line) bool {
stdin.WriteString(l.Buffer())
stdin.WriteRune('\n')
return true
})
@ -870,11 +866,7 @@ func doGoToAdjacentSelection(ctx context.Context, state *Peco, forward bool) {
}
found := false
selection.Ascend(func(it btree.Item) bool {
l, ok := it.(line.Line)
if !ok {
return true
}
selection.Ascend(func(l line.Line) bool {
id := l.ID()
if forward {
if id > currentLine && id < target {

View file

@ -13,6 +13,7 @@ import (
"github.com/peco/peco/hub"
"github.com/peco/peco/internal/keyseq"
"github.com/peco/peco/line"
"github.com/peco/peco/selection"
"github.com/stretchr/testify/require"
)
@ -201,7 +202,7 @@ func TestPagingActions(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.currentLineBuffer = NewMemoryBuffer(0)
action, ok := nameToActions[tt.action]
@ -549,7 +550,7 @@ func TestGHIssue574_PreviousSelectionLastLineNotUpdated(t *testing.T) {
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
// Set the current line buffer to our prepared buffer.
state.currentLineBuffer = mb
@ -648,7 +649,7 @@ func TestNextSelectionNavigation(t *testing.T) {
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.currentLineBuffer = mb
state.Selection().Add(lines[1]) // ID=20
@ -727,7 +728,7 @@ func TestGHIssue428_PgUpPgDnDefaultBindings(t *testing.T) {
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.currentLineBuffer = NewMemoryBuffer(0)
// Populate the keymap with defaults (no custom config).
@ -772,7 +773,7 @@ func TestGHIssue455_RefreshScreenSendsForceSync(t *testing.T) {
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.currentLineBuffer = NewMemoryBuffer(0)
doRefreshScreen(ctx, state, Event{})
@ -801,7 +802,7 @@ func TestDoFreezeResults(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
lines := makeLines("alpha", "beta", "gamma")
mb := NewMemoryBuffer(0)
@ -834,7 +835,7 @@ func TestDoFreezeResults(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.source = &Source{}
lines := makeLines("frozen1", "frozen2")
@ -856,7 +857,7 @@ func TestDoFreezeResults(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
origLines := makeLines("orig1", "orig2", "orig3")
origSource := &Source{}
@ -886,7 +887,7 @@ func TestDoFreezeResults(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.currentLineBuffer = NewMemoryBuffer(0)
doFreezeResults(ctx, state, Event{})
@ -901,7 +902,7 @@ func TestDoFreezeResults(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
doUnfreezeResults(ctx, state, Event{})
@ -1068,7 +1069,7 @@ func TestDoZoomInOut(t *testing.T) {
rHub := &recordingHub{}
state := New()
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
state.source = &Source{}
state.source.lines = source.lines
state.currentLineBuffer = filtered

View file

@ -12,6 +12,7 @@ import (
"github.com/peco/peco/hub"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/peco/peco/selection"
"github.com/stretchr/testify/require"
)
@ -277,7 +278,7 @@ func TestFrozenCacheInvalidation(t *testing.T) {
state := New()
rHub := &recordingHub{}
state.hub = rHub
state.selection = NewSelection()
state.selection = selection.New()
// Populate filters (needed by Filter.Work)
state.filters.Add(filter.NewIgnoreCase())

35
peco.go
View file

@ -16,13 +16,13 @@ import (
"context"
"github.com/google/btree"
"github.com/lestrrat-go/pdebug"
"github.com/peco/peco/filter"
"github.com/peco/peco/hub"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/peco/peco/selection"
"github.com/peco/peco/sig"
)
@ -74,9 +74,9 @@ type Peco struct {
readyCh chan struct{}
resultCh chan line.Line
screen Screen
selection *Selection
selection *selection.Set
selectionPrefix string
selectionRangeStart RangeStart
selectionRangeStart selection.RangeStart
exitZeroAndExit bool // True if --exit-0 is enabled
selectOneAndExit bool // True if --select-1 is enabled
selectOneTriggered atomic.Bool
@ -211,7 +211,7 @@ func New() *Peco {
readyCh: make(chan struct{}),
configReader: defaultConfigReader,
screen: NewTcellScreen(),
selection: NewSelection(),
selection: selection.New(),
maxScanBufferSize: bufio.MaxScanTokenSize,
}
}
@ -264,28 +264,11 @@ func (p *Peco) SetResultCh(ch chan line.Line) {
p.resultCh = ch
}
func (p *Peco) Selection() *Selection {
func (p *Peco) Selection() *selection.Set {
return p.selection
}
func (s RangeStart) Valid() bool {
return s.valid
}
func (s RangeStart) Value() int {
return s.val
}
func (s *RangeStart) SetValue(n int) {
s.val = n
s.valid = true
}
func (s *RangeStart) Reset() {
s.valid = false
}
func (p *Peco) SelectionRangeStart() *RangeStart {
func (p *Peco) SelectionRangeStart() *selection.RangeStart {
return &p.selectionRangeStart
}
@ -1086,11 +1069,7 @@ func (p *Peco) PrintResults() {
p.SetResultCh(make(chan line.Line))
go func() {
defer close(p.resultCh)
p.selection.Ascend(func(it btree.Item) bool {
l, ok := it.(line.Line)
if !ok {
return true
}
p.selection.Ascend(func(l line.Line) bool {
p.ResultCh() <- l
return true
})

View file

@ -19,6 +19,7 @@ import (
"github.com/peco/peco/internal/keyseq"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/peco/peco/selection"
"github.com/stretchr/testify/require"
)
@ -302,7 +303,7 @@ func TestIDGen(t *testing.T) {
lines = append(lines, line.NewRaw(idgen.Next(), fmt.Sprintf("%d", i), false, false))
}
sel := NewSelection()
sel := selection.New()
for _, l := range lines {
if sel.Has(l) {
t.Errorf("Collision detected %d", l.ID())

View file

@ -1,84 +0,0 @@
package peco
import (
"sync"
"github.com/google/btree"
"github.com/peco/peco/line"
)
// Selection stores the line ids that were selected by the user.
// The contents of the Selection is always sorted from smallest to
// largest line ID
type Selection struct {
mutex sync.RWMutex
tree *btree.BTree
}
// RangeStart tracks the starting position of a range selection.
type RangeStart struct {
val int
valid bool
}
// NewSelection creates a new empty Selection
func NewSelection() *Selection {
s := &Selection{}
s.Reset()
return s
}
// Add adds a new line to the selection. If the line already
// exists in the selection, it is silently ignored
func (s *Selection) Add(l line.Line) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.tree.ReplaceOrInsert(l)
}
// Copy copies all selected lines from s into dst.
func (s *Selection) Copy(dst *Selection) {
s.Ascend(func(it btree.Item) bool {
l, ok := it.(line.Line)
if !ok {
return true
}
dst.Add(l)
return true
})
}
// Remove removes the specified line from the selection
func (s *Selection) Remove(l line.Line) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.tree.Delete(l)
}
// Reset clears all selected indices from the selection.
func (s *Selection) Reset() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.tree = btree.New(32)
}
// Has reports whether the given line is in the selection.
func (s *Selection) Has(x line.Line) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.tree.Has(x)
}
// Len returns the number of selected lines.
func (s *Selection) Len() int {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.tree.Len()
}
// Ascend iterates over selected lines in ascending order, calling i for each.
func (s *Selection) Ascend(i btree.ItemIterator) {
s.mutex.RLock()
defer s.mutex.RUnlock()
s.tree.Ascend(i)
}

107
selection/selection.go Normal file
View file

@ -0,0 +1,107 @@
package selection
import (
"sync"
"github.com/google/btree"
"github.com/peco/peco/line"
)
// Set stores the line ids that were selected by the user.
// The contents of the Set is always sorted from smallest to
// largest line ID.
type Set struct {
mutex sync.RWMutex
tree *btree.BTree
}
// RangeStart tracks the starting position of a range selection.
type RangeStart struct {
val int
valid bool
}
// New creates a new empty Set.
func New() *Set {
s := &Set{}
s.Reset()
return s
}
// Add adds a new line to the selection. If the line already
// exists in the selection, it is silently ignored.
func (s *Set) Add(l line.Line) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.tree.ReplaceOrInsert(l)
}
// Copy copies all selected lines from s into dst.
func (s *Set) Copy(dst *Set) {
s.Ascend(func(l line.Line) bool {
dst.Add(l)
return true
})
}
// Remove removes the specified line from the selection.
func (s *Set) Remove(l line.Line) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.tree.Delete(l)
}
// Reset clears all selected indices from the selection.
func (s *Set) Reset() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.tree = btree.New(32)
}
// Has reports whether the given line is in the selection.
func (s *Set) Has(x line.Line) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.tree.Has(x)
}
// Len returns the number of selected lines.
func (s *Set) Len() int {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.tree.Len()
}
// Ascend iterates over selected lines in ascending order, calling fn for each.
func (s *Set) Ascend(fn func(line.Line) bool) {
s.mutex.RLock()
defer s.mutex.RUnlock()
s.tree.Ascend(func(it btree.Item) bool {
l, ok := it.(line.Line)
if !ok {
return true
}
return fn(l)
})
}
// Valid reports whether the RangeStart has been set.
func (s RangeStart) Valid() bool {
return s.valid
}
// Value returns the starting line index of the range.
func (s RangeStart) Value() int {
return s.val
}
// SetValue sets the starting position and marks it as valid.
func (s *RangeStart) SetValue(n int) {
s.val = n
s.valid = true
}
// Reset clears the range start, marking it as invalid.
func (s *RangeStart) Reset() {
s.valid = false
}

View file

@ -1,4 +1,4 @@
package peco
package selection
import (
"testing"
@ -8,7 +8,7 @@ import (
)
func TestSelection(t *testing.T) {
s := NewSelection()
s := New()
var i uint64
alice := line.NewRaw(i, "Alice", false, false)