From b638ff46fb958c7a731cf8302d5fc72b5021bbe0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 20 Mar 2026 23:06:16 +0900 Subject: [PATCH] Include match positions in GET / HTTP response Close #4726 --- CHANGELOG.md | 1 + src/terminal.go | 15 ++++++++++++--- test/test_server.rb | 25 +++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37546913..78395c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/terminal.go b/src/terminal.go index a93c7e66..94a02587 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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 { diff --git a/test/test_server.rb b/test/test_server.rb index 5252fa9b..2b2346ec 100644 --- a/test/test_server.rb +++ b/test/test_server.rb @@ -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] }