Add bolding of matching search results for #112, currently behind the beta-mode flag

This commit is contained in:
David Dworken 2023-09-24 15:57:39 -07:00
parent ca155b3a1f
commit beb907d59e
No known key found for this signature in database
3 changed files with 131 additions and 10 deletions

View File

@ -170,6 +170,21 @@ func BuildTableRow(ctx context.Context, columnNames []string, entry data.History
return row, nil
}
// Make a regex that matches the non-tokenized bits of the given query
func MakeRegexFromQuery(query string) string {
tokens := tokenize(strings.TrimSpace(query))
r := ""
for _, token := range tokens {
if !strings.HasPrefix(token, "-") && !containsUnescaped(token, ":") {
if r != "" {
r += "|"
}
r += fmt.Sprintf("(%s)", regexp.QuoteMeta(token))
}
}
return r
}
func stringArrayToAnyArray(arr []string) []any {
ret := make([]any, 0)
for _, item := range arr {
@ -711,10 +726,7 @@ func where(tx *gorm.DB, s string, v1 any, v2 any) *gorm.DB {
}
func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
tokens, err := tokenize(query)
if err != nil {
return nil, fmt.Errorf("failed to tokenize query: %w", err)
}
tokens := tokenize(query)
tx := db.Model(&data.HistoryEntry{}).Where("true")
for _, token := range tokens {
if strings.HasPrefix(token, "-") {
@ -891,11 +903,11 @@ func getAllCustomColumnNames(ctx context.Context) ([]string, error) {
return ccNames, nil
}
func tokenize(query string) ([]string, error) {
func tokenize(query string) []string {
if query == "" {
return []string{}, nil
return []string{}
}
return splitEscaped(query, ' ', -1), nil
return splitEscaped(query, ' ', -1)
}
func splitEscaped(query string, separator rune, maxSplit int) []string {

View File

@ -1,4 +1,5 @@
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table.go to add horizontal scrolling
// Also includes https://github.com/charmbracelet/bubbles/pull/397/files to support cell styling
package table
@ -31,6 +32,13 @@ type Model struct {
hcursor int
}
// CellPosition holds row and column indexes.
type CellPosition struct {
RowID int
Column int
IsRowSelected bool
}
// Row represents one line in the table.
type Row []string
@ -108,6 +116,32 @@ type Styles struct {
Header lipgloss.Style
Cell lipgloss.Style
Selected lipgloss.Style
// RenderCell is a low-level primitive for stylizing cells.
// It is responsible for rendering the selection style. Styles.Cell is ignored.
//
// Example implementation:
// s.RenderCell = func(model table.Model, value string, position table.CellPosition) string {
// cellStyle := s.Cell.Copy()
//
// switch {
// case position.IsRowSelected:
// return cellStyle.Background(lipgloss.Color("57")).Render(value)
// case position.Column == 1:
// return cellStyle.Foreground(lipgloss.Color("21")).Render(value)
// default:
// return cellStyle.Render(value)
// }
// }
RenderCell func(model Model, value string, position CellPosition) string
}
func (s Styles) renderCell(model Model, value string, position CellPosition) string {
if s.RenderCell != nil {
return s.RenderCell(model, value, position)
}
return s.Cell.Render(value)
}
// DefaultStyles returns a set of default style definitions for this table.
@ -445,21 +479,30 @@ func (m Model) headersView() string {
}
func (m *Model) renderRow(rowID int) string {
isRowSelected := rowID == m.cursor
var s = make([]string, 0, len(m.cols))
for i, value := range m.rows[rowID] {
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
position := CellPosition{
RowID: rowID,
Column: i,
IsRowSelected: isRowSelected,
}
var renderedCell string
if i == m.ColIndex(m.hcol) && m.hcursor > 0 {
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…")))
renderedCell = style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…"))
} else {
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…")))
renderedCell = style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))
}
renderedCell = m.styles.renderCell(*m, renderedCell, position)
s = append(s, renderedCell)
}
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
if rowID == m.cursor {
if isRowSelected {
return m.styles.Selected.Render(row)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
@ -27,6 +28,7 @@ import (
const TABLE_HEIGHT = 20
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
var CURRENT_QUERY_FOR_HIGHLIGHTING string = ""
var SELECTED_COMMAND string = ""
var baseStyle = lipgloss.NewStyle().
@ -207,6 +209,7 @@ func initialModel(ctx context.Context, initialQuery string) model {
if initialQuery != "" {
queryInput.SetValue(initialQuery)
}
CURRENT_QUERY_FOR_HIGHLIGHTING = initialQuery
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New()}
}
@ -319,6 +322,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.queryInput = i
searchQuery := m.queryInput.Value()
m.runQuery = &searchQuery
CURRENT_QUERY_FOR_HIGHLIGHTING = searchQuery
cmd3 := runQueryAndUpdateTable(m, false, false)
preventTableOverscrolling(m)
return m, tea.Batch(pendingCommands, cmd2, cmd3)
@ -579,6 +583,68 @@ func makeTable(ctx context.Context, rows []table.Row) (table.Model, error) {
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
if config.BetaMode {
s.RenderCell = func(model table.Model, value string, position table.CellPosition) string {
MATCH_NOTHING_REGEXP := regexp.MustCompile("a^")
var re *regexp.Regexp
CURRENT_QUERY_FOR_HIGHLIGHTING = strings.TrimSpace(CURRENT_QUERY_FOR_HIGHLIGHTING)
if CURRENT_QUERY_FOR_HIGHLIGHTING == "" {
re = MATCH_NOTHING_REGEXP
} else {
queryRegex := lib.MakeRegexFromQuery(CURRENT_QUERY_FOR_HIGHLIGHTING)
r, err := regexp.Compile(queryRegex)
if err != nil {
// Failed to compile the regex for highlighting matches, this should never happen. In this
// case, just use a regexp that matches nothing to ensure that the TUI doesn't crash.
re = MATCH_NOTHING_REGEXP
} else {
re = r
}
}
renderChunk := func(v string, isMatching, isLeftMost, isRightMost bool) string {
hctx.GetLogger().Infof("Rendering chunk=%#v with isMatching=%#v", v, isMatching)
baseStyle := lipgloss.NewStyle()
if position.IsRowSelected {
baseStyle = s.Selected.Copy()
}
if isLeftMost {
baseStyle = baseStyle.PaddingLeft(1)
}
if isRightMost {
baseStyle = baseStyle.PaddingRight(1)
}
if isMatching {
baseStyle = baseStyle.Bold(true)
}
return baseStyle.Render(v)
}
matches := re.FindAllStringIndex(value, -1)
if len(matches) == 0 {
return renderChunk(value, false, true, true)
}
ret := ""
lastIncludedIdx := 0
for _, match := range re.FindAllStringIndex(value, -1) {
matchStartIdx := match[0]
matchEndIdx := match[1]
beforeMatch := value[lastIncludedIdx:matchStartIdx]
if beforeMatch != "" {
ret += renderChunk(beforeMatch, false, lastIncludedIdx == 0, false)
}
match := value[matchStartIdx:matchEndIdx]
ret += renderChunk(match, true, matchStartIdx == 0, matchEndIdx == len(value))
lastIncludedIdx = matchEndIdx
}
if lastIncludedIdx != len(value) {
ret += renderChunk(value[lastIncludedIdx:], false, false, true)
}
hctx.GetLogger().Infof("bolded=%#v, original=%#v", ret, s.Cell.Render(value))
return ret
}
}
t.SetStyles(s)
t.Focus()
return t, nil