Include match positions in GET / HTTP response

Close #4726
This commit is contained in:
Junegunn Choi
2026-03-20 23:06:16 +09:00
parent 259e841a77
commit b638ff46fb
3 changed files with 38 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ CHANGELOG
- Improved the cache structure, reducing memory footprint per entry by 86x.
- With the reduced per-entry cost, the cache now has broader coverage.
- fish: Improved command history (CTRL-R) (#4703) (@bitraid)
- `GET /` HTTP endpoint now includes `positions` field in each match entry, providing the indices of matched characters for external highlighting (#4726)
- Bug fixes
- `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710)
- Fixed AWK tokenizer not treating a new line character as whitespace

View File

@@ -216,8 +216,9 @@ const (
)
type StatusItem struct {
Index int `json:"index"`
Text string `json:"text"`
Index int `json:"index"`
Text string `json:"text"`
Positions []int `json:"positions,omitempty"`
}
type Status struct {
@@ -7869,10 +7870,18 @@ func (t *Terminal) dumpItem(i *Item) StatusItem {
if i == nil {
return StatusItem{}
}
return StatusItem{
item := StatusItem{
Index: int(i.Index()),
Text: i.AsString(t.ansi),
}
if t.resultMerger.pattern != nil {
_, _, pos := t.resultMerger.pattern.MatchItem(i, true, t.slab)
if pos != nil {
sort.Ints(*pos)
item.Positions = *pos
}
}
return item
}
func (t *Terminal) tryLock(timeout time.Duration) bool {

View File

@@ -16,6 +16,31 @@ class TestServer < TestInteractive
assert_empty state[:query]
assert_equal({ index: 0, text: '1' }, state[:current])
# No positions when query is empty
state[:matches].each do |m|
assert_nil m[:positions]
end
assert_nil state[:current][:positions] if state[:current]
# Positions with a single-character query
Net::HTTP.post(fn.call, 'change-query(1)')
tmux.until { |lines| assert_equal 2, lines.match_count }
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
assert_equal [0], state[:current][:positions]
state[:matches].each do |m|
assert_includes m[:text], '1'
assert_equal [m[:text].index('1')], m[:positions]
end
# Positions with a multi-character query; verify sorted ascending
Net::HTTP.post(fn.call, 'change-query(10)')
tmux.until { |lines| assert_equal 1, lines.match_count }
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
assert_equal '10', state[:current][:text]
assert_equal [0, 1], state[:current][:positions]
assert_equal state[:current][:positions], state[:current][:positions].sort
# No match — no current item
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }