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

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 {