mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
add fuzzy search
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -176,15 +177,39 @@ func (m *TableDataModel) filterData() {
|
|||||||
m.Shared.FilteredData = make([][]string, len(m.Shared.TableData))
|
m.Shared.FilteredData = make([][]string, len(m.Shared.TableData))
|
||||||
copy(m.Shared.FilteredData, m.Shared.TableData)
|
copy(m.Shared.FilteredData, m.Shared.TableData)
|
||||||
} else {
|
} else {
|
||||||
m.Shared.FilteredData = [][]string{}
|
// Fuzzy search with scoring for rows
|
||||||
|
type rowMatch struct {
|
||||||
|
row []string
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches []rowMatch
|
||||||
searchLower := strings.ToLower(m.searchInput)
|
searchLower := strings.ToLower(m.searchInput)
|
||||||
|
|
||||||
for _, row := range m.Shared.TableData {
|
for _, row := range m.Shared.TableData {
|
||||||
|
bestScore := 0
|
||||||
|
// Check each cell in the row and take the best score
|
||||||
for _, cell := range row {
|
for _, cell := range row {
|
||||||
if strings.Contains(strings.ToLower(cell), searchLower) {
|
score := m.fuzzyScore(strings.ToLower(cell), searchLower)
|
||||||
m.Shared.FilteredData = append(m.Shared.FilteredData, row)
|
if score > bestScore {
|
||||||
break
|
bestScore = score
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bestScore > 0 {
|
||||||
|
matches = append(matches, rowMatch{row: row, score: bestScore})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score (highest first)
|
||||||
|
sort.Slice(matches, func(i, j int) bool {
|
||||||
|
return matches[i].score > matches[j].score
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract sorted rows
|
||||||
|
m.Shared.FilteredData = make([][]string, len(matches))
|
||||||
|
for i, match := range matches {
|
||||||
|
m.Shared.FilteredData[i] = match.row
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +218,74 @@ func (m *TableDataModel) filterData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fuzzyScore calculates a fuzzy match score between text and pattern
|
||||||
|
// Returns 0 for no match, higher scores for better matches
|
||||||
|
func (m *TableDataModel) fuzzyScore(text, pattern string) int {
|
||||||
|
if pattern == "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
textLen := len(text)
|
||||||
|
patternLen := len(pattern)
|
||||||
|
|
||||||
|
if patternLen > textLen {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match gets highest score
|
||||||
|
if text == pattern {
|
||||||
|
return 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix match gets high score
|
||||||
|
if strings.HasPrefix(text, pattern) {
|
||||||
|
return 900
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains match gets medium score
|
||||||
|
if strings.Contains(text, pattern) {
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuzzy character sequence matching
|
||||||
|
score := 0
|
||||||
|
textIdx := 0
|
||||||
|
patternIdx := 0
|
||||||
|
consecutiveMatches := 0
|
||||||
|
|
||||||
|
for textIdx < textLen && patternIdx < patternLen {
|
||||||
|
if text[textIdx] == pattern[patternIdx] {
|
||||||
|
score += 10
|
||||||
|
consecutiveMatches++
|
||||||
|
|
||||||
|
// Bonus for consecutive matches
|
||||||
|
if consecutiveMatches > 1 {
|
||||||
|
score += consecutiveMatches * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for matches at word boundaries
|
||||||
|
if textIdx == 0 || text[textIdx-1] == '_' || text[textIdx-1] == '-' || text[textIdx-1] == ' ' {
|
||||||
|
score += 20
|
||||||
|
}
|
||||||
|
|
||||||
|
patternIdx++
|
||||||
|
} else {
|
||||||
|
consecutiveMatches = 0
|
||||||
|
}
|
||||||
|
textIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must match all pattern characters
|
||||||
|
if patternIdx < patternLen {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for shorter text (more precise match)
|
||||||
|
score += (100 - textLen)
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
func (m *TableDataModel) View() string {
|
func (m *TableDataModel) View() string {
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -161,13 +162,32 @@ func (m *TableListModel) filterTables() {
|
|||||||
m.Shared.FilteredTables = make([]string, len(m.Shared.Tables))
|
m.Shared.FilteredTables = make([]string, len(m.Shared.Tables))
|
||||||
copy(m.Shared.FilteredTables, m.Shared.Tables)
|
copy(m.Shared.FilteredTables, m.Shared.Tables)
|
||||||
} else {
|
} else {
|
||||||
m.Shared.FilteredTables = []string{}
|
// Fuzzy search with scoring
|
||||||
|
type tableMatch struct {
|
||||||
|
name string
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches []tableMatch
|
||||||
searchLower := strings.ToLower(m.searchInput)
|
searchLower := strings.ToLower(m.searchInput)
|
||||||
|
|
||||||
for _, table := range m.Shared.Tables {
|
for _, table := range m.Shared.Tables {
|
||||||
if strings.Contains(strings.ToLower(table), searchLower) {
|
score := m.fuzzyScore(strings.ToLower(table), searchLower)
|
||||||
m.Shared.FilteredTables = append(m.Shared.FilteredTables, table)
|
if score > 0 {
|
||||||
|
matches = append(matches, tableMatch{name: table, score: score})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort by score (highest first)
|
||||||
|
sort.Slice(matches, func(i, j int) bool {
|
||||||
|
return matches[i].score > matches[j].score
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract sorted table names
|
||||||
|
m.Shared.FilteredTables = make([]string, len(matches))
|
||||||
|
for i, match := range matches {
|
||||||
|
m.Shared.FilteredTables[i] = match.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.selectedTable >= len(m.Shared.FilteredTables) {
|
if m.selectedTable >= len(m.Shared.FilteredTables) {
|
||||||
@@ -176,6 +196,74 @@ func (m *TableListModel) filterTables() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fuzzyScore calculates a fuzzy match score between text and pattern
|
||||||
|
// Returns 0 for no match, higher scores for better matches
|
||||||
|
func (m *TableListModel) fuzzyScore(text, pattern string) int {
|
||||||
|
if pattern == "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
textLen := len(text)
|
||||||
|
patternLen := len(pattern)
|
||||||
|
|
||||||
|
if patternLen > textLen {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match gets highest score
|
||||||
|
if text == pattern {
|
||||||
|
return 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix match gets high score
|
||||||
|
if strings.HasPrefix(text, pattern) {
|
||||||
|
return 900
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains match gets medium score
|
||||||
|
if strings.Contains(text, pattern) {
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuzzy character sequence matching
|
||||||
|
score := 0
|
||||||
|
textIdx := 0
|
||||||
|
patternIdx := 0
|
||||||
|
consecutiveMatches := 0
|
||||||
|
|
||||||
|
for textIdx < textLen && patternIdx < patternLen {
|
||||||
|
if text[textIdx] == pattern[patternIdx] {
|
||||||
|
score += 10
|
||||||
|
consecutiveMatches++
|
||||||
|
|
||||||
|
// Bonus for consecutive matches
|
||||||
|
if consecutiveMatches > 1 {
|
||||||
|
score += consecutiveMatches * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for matches at word boundaries
|
||||||
|
if textIdx == 0 || text[textIdx-1] == '_' || text[textIdx-1] == '-' {
|
||||||
|
score += 20
|
||||||
|
}
|
||||||
|
|
||||||
|
patternIdx++
|
||||||
|
} else {
|
||||||
|
consecutiveMatches = 0
|
||||||
|
}
|
||||||
|
textIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must match all pattern characters
|
||||||
|
if patternIdx < patternLen {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for shorter text (more precise match)
|
||||||
|
score += (100 - textLen)
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
func (m *TableListModel) getVisibleCount() int {
|
func (m *TableListModel) getVisibleCount() int {
|
||||||
reservedLines := 8
|
reservedLines := 8
|
||||||
if m.searching {
|
if m.searching {
|
||||||
|
|||||||
Reference in New Issue
Block a user