mirror of
https://github.com/ddworken/hishtory.git
synced 2024-12-23 23:39:02 +01:00
Add bolding of matching search results for #112, currently behind the beta-mode flag
This commit is contained in:
parent
56b2d464e4
commit
0f375aacfc
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user