From 2a8d21774af255867abe938998bb8a7474c5df53 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Fri, 20 Feb 2026 15:13:44 +0900 Subject: [PATCH 1/2] extract query package --- action_test.go | 5 +- caret.go | 36 ----------- caret_test.go | 67 --------------------- peco.go | 9 +-- query.go => query/query.go | 59 ++++++++++++++---- query_test.go => query/query_test.go | 90 +++++++++++++++++++++++----- 6 files changed, 129 insertions(+), 137 deletions(-) delete mode 100644 caret.go delete mode 100644 caret_test.go rename query.go => query/query.go (57%) rename query_test.go => query/query_test.go (80%) diff --git a/action_test.go b/action_test.go index d20435c..fc38d25 100644 --- a/action_test.go +++ b/action_test.go @@ -12,6 +12,7 @@ import ( "github.com/peco/peco/filter" "github.com/peco/peco/hub" "github.com/peco/peco/internal/keyseq" + "github.com/peco/peco/query" "github.com/peco/peco/line" "github.com/peco/peco/selection" "github.com/stretchr/testify/require" @@ -265,12 +266,12 @@ func TestViewAroundActionName(t *testing.T) { require.True(t, ok, "peco.ViewArround must remain registered for backward compatibility") } -func expectCaretPos(t *testing.T, c *Caret, expect int) { +func expectCaretPos(t *testing.T, c *query.Caret, expect int) { t.Helper() require.Equal(t, expect, c.Pos(), "Expected caret position %d, got %d", expect, c.Pos()) } -func expectQueryString(t *testing.T, q *Query, expect string) { +func expectQueryString(t *testing.T, q *query.Text, expect string) { t.Helper() require.Equal(t, expect, q.String(), "Expected '%s', got '%s'", expect, q.String()) } diff --git a/caret.go b/caret.go deleted file mode 100644 index b4c14c5..0000000 --- a/caret.go +++ /dev/null @@ -1,36 +0,0 @@ -package peco - -import "sync" - -// Caret tracks the cursor position within the query line. -type Caret struct { - mutex sync.Mutex - pos int -} - -// Pos returns the current caret position, thread-safe. -func (c *Caret) Pos() int { - c.mutex.Lock() - defer c.mutex.Unlock() - return c.pos -} - -// setPosNL sets the caret position without acquiring the mutex. -// The caller must already hold the lock. -func (c *Caret) setPosNL(p int) { - c.pos = p -} - -// SetPos sets the caret position, thread-safe. -func (c *Caret) SetPos(p int) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.setPosNL(p) -} - -// Move moves the caret by the given delta, thread-safe. -func (c *Caret) Move(diff int) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.setPosNL(c.pos + diff) -} diff --git a/caret_test.go b/caret_test.go deleted file mode 100644 index 7453e84..0000000 --- a/caret_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package peco - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCaretInitialPos(t *testing.T) { - t.Parallel() - var c Caret - require.Equal(t, 0, c.Pos()) -} - -func TestCaretSetPos(t *testing.T) { - t.Parallel() - var c Caret - c.SetPos(5) - require.Equal(t, 5, c.Pos()) - - c.SetPos(0) - require.Equal(t, 0, c.Pos()) - - c.SetPos(100) - require.Equal(t, 100, c.Pos()) -} - -func TestCaretMove(t *testing.T) { - t.Parallel() - var c Caret - c.SetPos(5) - - c.Move(3) - require.Equal(t, 8, c.Pos()) - - c.Move(-2) - require.Equal(t, 6, c.Pos()) - - // Move to negative territory - c.Move(-10) - require.Equal(t, -4, c.Pos()) -} - -func TestCaretMoveFromZero(t *testing.T) { - t.Parallel() - var c Caret - - c.Move(1) - require.Equal(t, 1, c.Pos()) - - c.Move(-1) - require.Equal(t, 0, c.Pos()) -} - -func TestCaretMultipleMoves(t *testing.T) { - t.Parallel() - var c Caret - for range 10 { - c.Move(1) - } - require.Equal(t, 10, c.Pos()) - - for range 5 { - c.Move(-1) - } - require.Equal(t, 5, c.Pos()) -} diff --git a/peco.go b/peco.go index ed23df6..1bf56e4 100644 --- a/peco.go +++ b/peco.go @@ -19,6 +19,7 @@ import ( "github.com/lestrrat-go/pdebug" "github.com/peco/peco/filter" "github.com/peco/peco/hub" + "github.com/peco/peco/query" "github.com/peco/peco/internal/util" "github.com/peco/peco/line" "github.com/peco/peco/pipeline" @@ -50,7 +51,7 @@ type Peco struct { args []string bufferSize int - caret Caret + caret query.Caret // Config contains the values read in from config file config Config currentLineBuffer Buffer @@ -69,7 +70,7 @@ type Peco struct { onCancel OnCancelBehavior printQuery bool prompt string - query Query + query query.Text queryExec QueryExecState readyCh chan struct{} resultCh chan line.Line @@ -306,7 +307,7 @@ func (p *Peco) Filters() *filter.Set { return &p.filters } -func (p *Peco) Query() *Query { +func (p *Peco) Query() *query.Text { return &p.query } @@ -314,7 +315,7 @@ func (p *Peco) QueryExec() *QueryExecState { return &p.queryExec } -func (p *Peco) Caret() *Caret { +func (p *Peco) Caret() *query.Caret { return &p.caret } diff --git a/query.go b/query/query.go similarity index 57% rename from query.go rename to query/query.go index df5f60c..5d0178a 100644 --- a/query.go +++ b/query/query.go @@ -1,35 +1,35 @@ -package peco +package query import "sync" -// Query holds the current query string and an optional saved query +// Text holds the current query string and an optional saved query // for restore-after-cancel behavior. -type Query struct { +type Text struct { query []rune savedQuery []rune mutex sync.Mutex } -func (q *Query) Set(s string) { +func (q *Text) Set(s string) { q.mutex.Lock() defer q.mutex.Unlock() q.query = []rune(s) } -func (q *Query) Reset() { +func (q *Text) Reset() { q.mutex.Lock() defer q.mutex.Unlock() q.query = []rune(nil) } -func (q *Query) RestoreSavedQuery() { +func (q *Text) RestoreSavedQuery() { q.mutex.Lock() defer q.mutex.Unlock() q.query = q.savedQuery q.savedQuery = []rune(nil) } -func (q *Query) SaveQuery() { +func (q *Text) SaveQuery() { q.mutex.Lock() defer q.mutex.Unlock() q.savedQuery = q.query @@ -37,7 +37,7 @@ func (q *Query) SaveQuery() { } // DeleteRange deletes runes in the range [start, end) from the query with boundary validation. -func (q *Query) DeleteRange(start, end int) { +func (q *Text) DeleteRange(start, end int) { q.mutex.Lock() defer q.mutex.Unlock() if start == -1 { @@ -59,20 +59,20 @@ func (q *Query) DeleteRange(start, end int) { q.query = q.query[:l-(end-start)] } -func (q *Query) String() string { +func (q *Text) String() string { q.mutex.Lock() defer q.mutex.Unlock() return string(q.query) } -func (q *Query) Len() int { +func (q *Text) Len() int { q.mutex.Lock() defer q.mutex.Unlock() return len(q.query) } // RuneSlice returns a copy of the query runes -func (q *Query) RuneSlice() []rune { +func (q *Text) RuneSlice() []rune { q.mutex.Lock() defer q.mutex.Unlock() out := make([]rune, len(q.query)) @@ -80,7 +80,7 @@ func (q *Query) RuneSlice() []rune { return out } -func (q *Query) RuneAt(where int) rune { +func (q *Text) RuneAt(where int) rune { q.mutex.Lock() defer q.mutex.Unlock() if where < 0 || where >= len(q.query) { @@ -90,7 +90,7 @@ func (q *Query) RuneAt(where int) rune { } // InsertAt inserts a rune at the specified position in the query. -func (q *Query) InsertAt(ch rune, where int) { +func (q *Text) InsertAt(ch rune, where int) { q.mutex.Lock() defer q.mutex.Unlock() @@ -106,3 +106,36 @@ func (q *Query) InsertAt(ch rune, where int) { copy(buf[where+1:], sq[where:]) q.query = buf } + +// Caret tracks the cursor position within the query line. +type Caret struct { + mutex sync.Mutex + pos int +} + +// Pos returns the current caret position, thread-safe. +func (c *Caret) Pos() int { + c.mutex.Lock() + defer c.mutex.Unlock() + return c.pos +} + +// setPosNL sets the caret position without acquiring the mutex. +// The caller must already hold the lock. +func (c *Caret) setPosNL(p int) { + c.pos = p +} + +// SetPos sets the caret position, thread-safe. +func (c *Caret) SetPos(p int) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.setPosNL(p) +} + +// Move moves the caret by the given delta, thread-safe. +func (c *Caret) Move(diff int) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.setPosNL(c.pos + diff) +} diff --git a/query_test.go b/query/query_test.go similarity index 80% rename from query_test.go rename to query/query_test.go index 692f574..eb2968c 100644 --- a/query_test.go +++ b/query/query_test.go @@ -1,4 +1,4 @@ -package peco +package query import ( "testing" @@ -21,7 +21,7 @@ func TestQuerySetAndString(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set(tt.input) require.Equal(t, tt.input, q.String()) }) @@ -30,7 +30,7 @@ func TestQuerySetAndString(t *testing.T) { func TestQueryLen(t *testing.T) { t.Parallel() - var q Query + var q Text require.Equal(t, 0, q.Len()) q.Set("hello") @@ -43,7 +43,7 @@ func TestQueryLen(t *testing.T) { func TestQueryReset(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set("hello") require.Equal(t, 5, q.Len()) @@ -54,7 +54,7 @@ func TestQueryReset(t *testing.T) { func TestQuerySaveAndRestore(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set("original") q.SaveQuery() @@ -72,7 +72,7 @@ func TestQuerySaveAndRestore(t *testing.T) { func TestQuerySaveAndRestoreEmpty(t *testing.T) { t.Parallel() - var q Query + var q Text // Save an empty query q.SaveQuery() require.Equal(t, "", q.String()) @@ -104,7 +104,7 @@ func TestQueryDeleteRange(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set(tt.initial) q.DeleteRange(tt.start, tt.end) require.Equal(t, tt.expected, q.String()) @@ -114,7 +114,7 @@ func TestQueryDeleteRange(t *testing.T) { func TestQueryRuneSlice(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set("hello") runes := q.RuneSlice() @@ -127,14 +127,14 @@ func TestQueryRuneSlice(t *testing.T) { func TestQueryRuneSliceEmpty(t *testing.T) { t.Parallel() - var q Query + var q Text runes := q.RuneSlice() require.Empty(t, runes) } func TestQueryRuneAt(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set("hello") require.Equal(t, 'h', q.RuneAt(0)) @@ -144,7 +144,7 @@ func TestQueryRuneAt(t *testing.T) { func TestQueryRuneAtUnicode(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set("あいう") require.Equal(t, 'あ', q.RuneAt(0)) @@ -154,7 +154,7 @@ func TestQueryRuneAtUnicode(t *testing.T) { func TestQueryRuneAtOutOfBounds(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set("hello") // Out-of-bounds index returns zero rune without panicking @@ -166,7 +166,7 @@ func TestQueryRuneAtOutOfBounds(t *testing.T) { require.Equal(t, rune(0), q.RuneAt(-100)) // Empty query: any index returns zero rune - var empty Query + var empty Text require.Equal(t, rune(0), empty.RuneAt(0)) require.Equal(t, rune(0), empty.RuneAt(-1)) } @@ -189,7 +189,7 @@ func TestQueryInsertAt(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var q Query + var q Text q.Set(tt.initial) q.InsertAt(tt.ch, tt.where) require.Equal(t, tt.expected, q.String()) @@ -199,7 +199,7 @@ func TestQueryInsertAt(t *testing.T) { func TestQueryMultipleInserts(t *testing.T) { t.Parallel() - var q Query + var q Text // Build "abc" by inserting one character at a time q.InsertAt('a', 0) q.InsertAt('b', 1) @@ -214,3 +214,63 @@ func TestQueryMultipleInserts(t *testing.T) { q.InsertAt('X', 2) require.Equal(t, "0aXbc", q.String()) } + +func TestCaretInitialPos(t *testing.T) { + t.Parallel() + var c Caret + require.Equal(t, 0, c.Pos()) +} + +func TestCaretSetPos(t *testing.T) { + t.Parallel() + var c Caret + c.SetPos(5) + require.Equal(t, 5, c.Pos()) + + c.SetPos(0) + require.Equal(t, 0, c.Pos()) + + c.SetPos(100) + require.Equal(t, 100, c.Pos()) +} + +func TestCaretMove(t *testing.T) { + t.Parallel() + var c Caret + c.SetPos(5) + + c.Move(3) + require.Equal(t, 8, c.Pos()) + + c.Move(-2) + require.Equal(t, 6, c.Pos()) + + // Move to negative territory + c.Move(-10) + require.Equal(t, -4, c.Pos()) +} + +func TestCaretMoveFromZero(t *testing.T) { + t.Parallel() + var c Caret + + c.Move(1) + require.Equal(t, 1, c.Pos()) + + c.Move(-1) + require.Equal(t, 0, c.Pos()) +} + +func TestCaretMultipleMoves(t *testing.T) { + t.Parallel() + var c Caret + for range 10 { + c.Move(1) + } + require.Equal(t, 10, c.Pos()) + + for range 5 { + c.Move(-1) + } + require.Equal(t, 5, c.Pos()) +} From ebaf55dafaddb5ed9b6261f5bc5452995cff418a Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Fri, 20 Feb 2026 15:15:53 +0900 Subject: [PATCH 2/2] appease linter --- action_test.go | 2 +- peco.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/action_test.go b/action_test.go index fc38d25..1e84399 100644 --- a/action_test.go +++ b/action_test.go @@ -12,8 +12,8 @@ import ( "github.com/peco/peco/filter" "github.com/peco/peco/hub" "github.com/peco/peco/internal/keyseq" - "github.com/peco/peco/query" "github.com/peco/peco/line" + "github.com/peco/peco/query" "github.com/peco/peco/selection" "github.com/stretchr/testify/require" ) diff --git a/peco.go b/peco.go index 1bf56e4..5946d2c 100644 --- a/peco.go +++ b/peco.go @@ -19,10 +19,10 @@ import ( "github.com/lestrrat-go/pdebug" "github.com/peco/peco/filter" "github.com/peco/peco/hub" - "github.com/peco/peco/query" "github.com/peco/peco/internal/util" "github.com/peco/peco/line" "github.com/peco/peco/pipeline" + "github.com/peco/peco/query" "github.com/peco/peco/selection" "github.com/peco/peco/sig" )