Fix highlight case handling flagged by review bots · Entire
Fix highlight case handling flagged by review bots
351af25→main·
evisdren·4d ago·2 files·+44 added/-18 removed
Highlighting now respects --case-sensitive instead of always case-folding, so terminal output doesn't mark case variants the search didn't match. Case folding is also restricted to pure-ASCII input: equal total byte length after ToLower doesn't guarantee per-offset alignment for Unicode, so non-ASCII lines fall back to exact matching.
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com
Changes
2
cmd/entire/cli
Msearch_cmd.go+23/-10
Msearch_cmd_test.go+21/-8
8 unmodified lines
9
10
11
12
13
14
15
483 unmodified lines
499
500
501
501
502
503
504
505
320 unmodified lines
826
827
828
828
829
830
831
832
40 unmodified lines
873
874
875
875
876
877
878
879
24 unmodified lines
904
905
906
906
907
908
909
910
907
908
909
910
911
912
913
914
915
916
917
915
916
918
919
920
921
922
15 unmodified lines
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
8 unmodified lines
"sort"
"strings"
"time"
"unicode/utf8"
tea "charm.land/bubbletea/v2"
"github.com/entireio/cli/cmd/entire/cli/api"
483 unmodified lines
return writeCodeSearchJSON(w, resp)
}
writeCodeSearchText(w, resp, newStatusStyles(w))
writeCodeSearchText(w, resp, newStatusStyles(w), opts.caseSensitive)
return nil
}
320 unmodified lines
// style): a colored "repo:path" header per file, indented line-numbered
// matches beneath it, and a dimmed stats footer. Colors are applied only when
// the writer supports them (styles.colorEnabled); piped output stays plain.
func writeCodeSearchText(w io.Writer, resp *codesearch.SearchResponse, styles statusStyles) {
func writeCodeSearchText(w io.Writer, resp *codesearch.SearchResponse, styles statusStyles, caseSensitive bool) {
if len(resp.Results) == 0 {
if len(resp.FailedJurisdictions) > 0 {
fmt.Fprintf(w, "No code search results found (some regions failed: %s)\n",
40 unmodified lines
line = string(runes[:maxContextLineLen]) + "…"
}
lineNo := styles.render(styles.dim, fmt.Sprintf("%d:", r.Line))
fmt.Fprintf(w, " %s %s\n", lineNo, highlightCodeMatches(line, resp.Query, styles))
fmt.Fprintf(w, " %s %s\n", lineNo, highlightCodeMatches(line, resp.Query, styles, caseSensitive))
shown++
}
// ponytail: overflow counts only what this page fetched (peregrine
24 unmodified lines
}
// highlightCodeMatches bold-red highlights occurrences of query in line
// (grep convention). Matching is case-insensitive when lowercasing doesn't
// change byte lengths (it can for some Unicode); otherwise it falls back to
// exact matching so byte offsets stay aligned. Returns line unchanged when
// color is disabled or there's nothing to highlight.
func highlightCodeMatches(line, query string, styles statusStyles) string {
// (grep convention). Matching mirrors the search: case-insensitive unless
// caseSensitive is set. Case folding is only applied when both strings are
// pure ASCII, since Unicode case mappings can change byte widths and
// misalign offsets against the original line; non-ASCII input falls back to
// exact matching. Returns line unchanged when color is disabled or there's
// nothing to highlight.
func highlightCodeMatches(line, query string, styles statusStyles, caseSensitive bool) string {
if !styles.colorEnabled || query == "" {
return line
}
haystack, needle := line, query
if l, q := strings.ToLower(line), strings.ToLower(query); len(l) == len(line) && len(q) == len(query) {
haystack, needle = l, q
}
if !caseSensitive && isASCII(line) && isASCII(query) {
haystack, needle = strings.ToLower(line), strings.ToLower(query)
}
matchStyle := styles.red.Bold(true)
var b strings.Builder
15 unmodified lines
return b.String()
}
// isASCII reports whether s contains only ASCII bytes.
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}
// writeSearchJSON writes client-side paginated search results as JSON.
func writeSearchJSON(w io.Writer, resp *search.Response, limit, page int) error {
if limit <= 0 {