mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
feat(tests): add comprehensive test suite, update deps and Go 1.26.1
- Add 20+ tests for utility functions, SharedData, and Model - Tests cover: LoadTables, LoadTableData, UpdateCell, pagination, table inference, focus/blur, empty database, invalid indices - Update Go to 1.26.1, upgrade all dependencies - Replace custom Min/Max with Go builtin min/max - Format all files with goimports - Add staticcheck to CI workflow
This commit is contained in:
@@ -522,20 +522,6 @@ func WrapText(text string, width int) []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func InitialModel(db *sql.DB, opts ...Option) *Model {
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
|
||||
428
internal/app/app_test.go
Normal file
428
internal/app/app_test.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestTruncateString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
maxLen int
|
||||
want string
|
||||
}{
|
||||
{"short string", "hello", 10, "hello"},
|
||||
{"exact length", "hello", 5, "hello"},
|
||||
{"needs truncation", "hello world", 8, "hello..."},
|
||||
{"empty string", "", 5, ""},
|
||||
{"min truncation", "abcdef", 4, "a..."},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := TruncateString(tt.input, tt.maxLen)
|
||||
if got != tt.want {
|
||||
t.Errorf("TruncateString(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
width int
|
||||
want []string
|
||||
}{
|
||||
{"short text", "hello", 20, []string{"hello"}},
|
||||
{"wrap at word boundary", "hello world foo", 12, []string{"hello world", "foo"}},
|
||||
{"zero width", "hello", 0, []string{"hello"}},
|
||||
{"empty text", "", 10, []string{""}},
|
||||
{"single long word", "abcdefghij", 5, []string{"abcde", "fghij"}},
|
||||
{"multiple words wrapping", "one two three four", 10, []string{"one two", "three four"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := WrapText(tt.text, tt.width)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("WrapText(%q, %d) returned %d lines, want %d: %v", tt.text, tt.width, len(got), len(tt.want), got)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("WrapText(%q, %d)[%d] = %q, want %q", tt.text, tt.width, i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextID(t *testing.T) {
|
||||
// nextID should return monotonically increasing values
|
||||
id1 := nextID()
|
||||
id2 := nextID()
|
||||
id3 := nextID()
|
||||
|
||||
if id2 <= id1 {
|
||||
t.Errorf("nextID() not monotonically increasing: %d, %d", id1, id2)
|
||||
}
|
||||
if id3 <= id2 {
|
||||
t.Errorf("nextID() not monotonically increasing: %d, %d", id2, id3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAppKeyMap(t *testing.T) {
|
||||
km := DefaultAppKeyMap()
|
||||
|
||||
if len(km.Quit.Keys()) == 0 {
|
||||
t.Error("Quit keybinding has no keys")
|
||||
}
|
||||
if len(km.Suspend.Keys()) == 0 {
|
||||
t.Error("Suspend keybinding has no keys")
|
||||
}
|
||||
if len(km.ToggleHelp.Keys()) == 0 {
|
||||
t.Error("ToggleHelp keybinding has no keys")
|
||||
}
|
||||
}
|
||||
|
||||
// createTestDB creates an in-memory SQLite database with test data
|
||||
func createTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test database: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
);
|
||||
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
||||
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
||||
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
||||
|
||||
CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
price REAL
|
||||
);
|
||||
INSERT INTO products (title, price) VALUES ('Widget', 9.99);
|
||||
INSERT INTO products (title, price) VALUES ('Gadget', 19.99);
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test data: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestNewSharedData(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if shared.DB != db {
|
||||
t.Error("NewSharedData should store the database reference")
|
||||
}
|
||||
if shared.Width != 80 || shared.Height != 24 {
|
||||
t.Errorf("default dimensions should be 80x24, got %dx%d", shared.Width, shared.Height)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataLoadTables(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(shared.Tables) != 2 {
|
||||
t.Fatalf("expected 2 tables, got %d: %v", len(shared.Tables), shared.Tables)
|
||||
}
|
||||
|
||||
// Tables should be sorted alphabetically
|
||||
if shared.Tables[0] != "products" || shared.Tables[1] != "users" {
|
||||
t.Errorf("expected [products, users], got %v", shared.Tables)
|
||||
}
|
||||
|
||||
// FilteredTables should be a copy of Tables
|
||||
if len(shared.FilteredTables) != len(shared.Tables) {
|
||||
t.Errorf("FilteredTables length mismatch: %d vs %d", len(shared.FilteredTables), len(shared.Tables))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataLoadTableData(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
|
||||
// Load "users" table (index 1 since sorted alphabetically)
|
||||
shared.SelectedTable = 1
|
||||
if err := shared.LoadTableData(); err != nil {
|
||||
t.Fatalf("LoadTableData() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(shared.Columns) != 3 {
|
||||
t.Fatalf("expected 3 columns, got %d: %v", len(shared.Columns), shared.Columns)
|
||||
}
|
||||
if shared.Columns[0] != "id" || shared.Columns[1] != "name" || shared.Columns[2] != "email" {
|
||||
t.Errorf("unexpected columns: %v", shared.Columns)
|
||||
}
|
||||
|
||||
if shared.TotalRows != 3 {
|
||||
t.Errorf("expected 3 total rows, got %d", shared.TotalRows)
|
||||
}
|
||||
|
||||
if len(shared.TableData) != 3 {
|
||||
t.Fatalf("expected 3 data rows, got %d", len(shared.TableData))
|
||||
}
|
||||
|
||||
// Check primary keys detected
|
||||
if len(shared.PrimaryKeys) != 1 || shared.PrimaryKeys[0] != "id" {
|
||||
t.Errorf("expected primary key [id], got %v", shared.PrimaryKeys)
|
||||
}
|
||||
|
||||
// Should not be marked as query result
|
||||
if shared.IsQueryResult {
|
||||
t.Error("regular table load should not be marked as query result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataLoadTableDataInvalidIndex(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
|
||||
shared.SelectedTable = 99
|
||||
if err := shared.LoadTableData(); err == nil {
|
||||
t.Error("LoadTableData() should fail with invalid table index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataUpdateCell(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
|
||||
// Load users table
|
||||
shared.SelectedTable = 1
|
||||
if err := shared.LoadTableData(); err != nil {
|
||||
t.Fatalf("LoadTableData() failed: %v", err)
|
||||
}
|
||||
|
||||
// Update name of first user (row 0, col 1 = name)
|
||||
if err := shared.UpdateCell(0, 1, "Alicia"); err != nil {
|
||||
t.Fatalf("UpdateCell() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify local data updated
|
||||
if shared.FilteredData[0][1] != "Alicia" {
|
||||
t.Errorf("FilteredData not updated, got %q", shared.FilteredData[0][1])
|
||||
}
|
||||
|
||||
// Verify database updated
|
||||
var name string
|
||||
err := db.QueryRow("SELECT name FROM users WHERE id = 1").Scan(&name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to verify update: %v", err)
|
||||
}
|
||||
if name != "Alicia" {
|
||||
t.Errorf("database not updated, got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataUpdateCellInvalidIndex(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
shared.SelectedTable = 1
|
||||
if err := shared.LoadTableData(); err != nil {
|
||||
t.Fatalf("LoadTableData() failed: %v", err)
|
||||
}
|
||||
|
||||
if err := shared.UpdateCell(99, 0, "value"); err == nil {
|
||||
t.Error("UpdateCell() should fail with invalid row index")
|
||||
}
|
||||
if err := shared.UpdateCell(0, 99, "value"); err == nil {
|
||||
t.Error("UpdateCell() should fail with invalid column index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataPagination(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// Insert enough rows to test pagination
|
||||
for i := 0; i < 25; i++ {
|
||||
_, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)",
|
||||
"User"+string(rune('A'+i)), "user@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert test data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
|
||||
shared.SelectedTable = 1
|
||||
shared.CurrentPage = 0
|
||||
if err := shared.LoadTableData(); err != nil {
|
||||
t.Fatalf("LoadTableData() page 0 failed: %v", err)
|
||||
}
|
||||
|
||||
if shared.TotalRows != 28 { // 3 original + 25 new
|
||||
t.Errorf("expected 28 total rows, got %d", shared.TotalRows)
|
||||
}
|
||||
if len(shared.TableData) != PageSize {
|
||||
t.Errorf("page 0 should have %d rows, got %d", PageSize, len(shared.TableData))
|
||||
}
|
||||
|
||||
// Load page 2
|
||||
shared.CurrentPage = 1
|
||||
if err := shared.LoadTableData(); err != nil {
|
||||
t.Fatalf("LoadTableData() page 1 failed: %v", err)
|
||||
}
|
||||
if len(shared.TableData) != 8 { // 28 - 20 = 8
|
||||
t.Errorf("page 1 should have 8 rows, got %d", len(shared.TableData))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitialModel(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
m := InitialModel(db)
|
||||
if m.Err() != nil {
|
||||
t.Fatalf("InitialModel() returned error: %v", m.Err())
|
||||
}
|
||||
if m.width != 80 || m.height != 24 {
|
||||
t.Errorf("default dimensions should be 80x24, got %dx%d", m.width, m.height)
|
||||
}
|
||||
if !m.Focused() {
|
||||
t.Error("model should be focused by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitialModelWithOptions(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
km := DefaultAppKeyMap()
|
||||
m := InitialModel(db, WithKeyMap(km), WithDimensions(120, 40))
|
||||
if m.Err() != nil {
|
||||
t.Fatalf("InitialModel() returned error: %v", m.Err())
|
||||
}
|
||||
if m.width != 120 || m.height != 40 {
|
||||
t.Errorf("custom dimensions should be 120x40, got %dx%d", m.width, m.height)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelFocusBlur(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
m := InitialModel(db)
|
||||
if !m.Focused() {
|
||||
t.Error("model should be focused initially")
|
||||
}
|
||||
|
||||
m.Blur()
|
||||
if m.Focused() {
|
||||
t.Error("model should not be focused after Blur()")
|
||||
}
|
||||
|
||||
m.Focus()
|
||||
if !m.Focused() {
|
||||
t.Error("model should be focused after Focus()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInferTableFromQueryResult(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() failed: %v", err)
|
||||
}
|
||||
|
||||
// Set up columns matching the users table
|
||||
shared.Columns = []string{"id", "name", "email"}
|
||||
shared.IsQueryResult = true
|
||||
shared.FilteredData = [][]string{{"1", "Alice", "alice@example.com"}}
|
||||
|
||||
tableName, err := shared.inferTableFromQueryResult(0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("inferTableFromQueryResult() failed: %v", err)
|
||||
}
|
||||
if tableName != "users" {
|
||||
t.Errorf("expected 'users', got %q", tableName)
|
||||
}
|
||||
|
||||
// Verify it was cached
|
||||
if shared.QueryTableName != "users" {
|
||||
t.Errorf("QueryTableName should be cached as 'users', got %q", shared.QueryTableName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTableInfo(t *testing.T) {
|
||||
db := createTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
cols, pks, err := shared.getTableInfo("users")
|
||||
if err != nil {
|
||||
t.Fatalf("getTableInfo() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(cols) != 3 {
|
||||
t.Errorf("expected 3 columns, got %d", len(cols))
|
||||
}
|
||||
if len(pks) != 1 || pks[0] != "id" {
|
||||
t.Errorf("expected primary key [id], got %v", pks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedDataEmptyDatabase(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
t.Fatalf("LoadTables() on empty db failed: %v", err)
|
||||
}
|
||||
|
||||
if len(shared.Tables) != 0 {
|
||||
t.Errorf("expected 0 tables in empty db, got %d", len(shared.Tables))
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type EditCellModel struct {
|
||||
@@ -151,7 +151,7 @@ func (m *EditCellModel) View() string {
|
||||
|
||||
content := fmt.Sprintf("%s\n\n", TitleStyle.Render(fmt.Sprintf("Edit Cell: %s", columnName)))
|
||||
content += fmt.Sprintf("Value: %s\n\n", m.input.View())
|
||||
|
||||
|
||||
if m.showFullHelp {
|
||||
content += m.help.FullHelpView(m.keyMap.FullHelp())
|
||||
} else {
|
||||
@@ -159,4 +159,4 @@ func (m *EditCellModel) View() string {
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
// EditCellKeyMap defines keybindings for the edit cell view
|
||||
type EditCellKeyMap struct {
|
||||
Save key.Binding
|
||||
Cancel key.Binding
|
||||
CursorLeft key.Binding
|
||||
CursorRight key.Binding
|
||||
WordLeft key.Binding
|
||||
WordRight key.Binding
|
||||
LineStart key.Binding
|
||||
LineEnd key.Binding
|
||||
DeleteWord key.Binding
|
||||
DeleteChar key.Binding
|
||||
ToggleHelp key.Binding
|
||||
Save key.Binding
|
||||
Cancel key.Binding
|
||||
CursorLeft key.Binding
|
||||
CursorRight key.Binding
|
||||
WordLeft key.Binding
|
||||
WordRight key.Binding
|
||||
LineStart key.Binding
|
||||
LineEnd key.Binding
|
||||
DeleteWord key.Binding
|
||||
DeleteChar key.Binding
|
||||
ToggleHelp key.Binding
|
||||
}
|
||||
|
||||
// DefaultEditCellKeyMap returns the default keybindings for edit cell
|
||||
@@ -79,4 +79,4 @@ func (k EditCellKeyMap) FullHelp() [][]key.Binding {
|
||||
{k.CursorLeft, k.CursorRight, k.WordLeft, k.WordRight},
|
||||
{k.LineStart, k.LineEnd, k.DeleteWord, k.DeleteChar, k.ToggleHelp},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ func (m *QueryModel) View() string {
|
||||
content.WriteString("\n")
|
||||
|
||||
// Data rows with scrolling
|
||||
visibleCount := Max(1, m.Shared.Height-10)
|
||||
visibleCount := max(1, m.Shared.Height-10)
|
||||
startIdx := 0
|
||||
|
||||
// Adjust start index if selected row is out of view
|
||||
@@ -426,7 +426,7 @@ func (m *QueryModel) View() string {
|
||||
startIdx = m.selectedRow - visibleCount + 1
|
||||
}
|
||||
|
||||
endIdx := Min(len(m.results), startIdx+visibleCount)
|
||||
endIdx := min(len(m.results), startIdx+visibleCount)
|
||||
|
||||
for i := range endIdx {
|
||||
if i < startIdx {
|
||||
@@ -465,4 +465,3 @@ func (m *QueryModel) View() string {
|
||||
|
||||
return content.String()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,25 +8,25 @@ import "github.com/charmbracelet/bubbles/key"
|
||||
// - G: go to end (single 'G' press)
|
||||
type QueryKeyMap struct {
|
||||
// Input mode keys
|
||||
Execute key.Binding
|
||||
Escape key.Binding
|
||||
CursorLeft key.Binding
|
||||
CursorRight key.Binding
|
||||
WordLeft key.Binding
|
||||
WordRight key.Binding
|
||||
LineStart key.Binding
|
||||
LineEnd key.Binding
|
||||
DeleteWord key.Binding
|
||||
|
||||
Execute key.Binding
|
||||
Escape key.Binding
|
||||
CursorLeft key.Binding
|
||||
CursorRight key.Binding
|
||||
WordLeft key.Binding
|
||||
WordRight key.Binding
|
||||
LineStart key.Binding
|
||||
LineEnd key.Binding
|
||||
DeleteWord key.Binding
|
||||
|
||||
// Results mode keys
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Enter key.Binding
|
||||
EditQuery key.Binding
|
||||
GoToStart key.Binding
|
||||
GoToEnd key.Binding
|
||||
Back key.Binding
|
||||
ToggleHelp key.Binding
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Enter key.Binding
|
||||
EditQuery key.Binding
|
||||
GoToStart key.Binding
|
||||
GoToEnd key.Binding
|
||||
Back key.Binding
|
||||
ToggleHelp key.Binding
|
||||
}
|
||||
|
||||
// DefaultQueryKeyMap returns the default keybindings for query view
|
||||
@@ -69,7 +69,7 @@ func DefaultQueryKeyMap() QueryKeyMap {
|
||||
key.WithKeys("ctrl+w"),
|
||||
key.WithHelp("ctrl+w", "delete word"),
|
||||
),
|
||||
|
||||
|
||||
// Results mode
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
@@ -119,4 +119,4 @@ func (k QueryKeyMap) FullHelp() [][]key.Binding {
|
||||
{k.CursorLeft, k.CursorRight, k.WordLeft, k.WordRight},
|
||||
{k.LineStart, k.LineEnd, k.DeleteWord, k.ToggleHelp},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type RowDetailModel struct {
|
||||
@@ -171,7 +171,7 @@ func (m *RowDetailModel) View() string {
|
||||
if availableWidth < 20 {
|
||||
availableWidth = 20 // Minimum width
|
||||
}
|
||||
|
||||
|
||||
if len(value) > availableWidth {
|
||||
// Wrap long values
|
||||
lines := WrapText(value, availableWidth)
|
||||
@@ -195,4 +195,4 @@ func (m *RowDetailModel) View() string {
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +66,4 @@ func (k RowDetailKeyMap) FullHelp() [][]key.Binding {
|
||||
{k.Up, k.Down, k.Enter},
|
||||
{k.Escape, k.Back, k.GoToStart, k.GoToEnd, k.ToggleHelp},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type TableDataModel struct {
|
||||
@@ -265,10 +265,10 @@ func (m *TableDataModel) filterData() {
|
||||
row []string
|
||||
score int
|
||||
}
|
||||
|
||||
|
||||
var matches []rowMatch
|
||||
searchLower := strings.ToLower(searchValue)
|
||||
|
||||
|
||||
for _, row := range m.Shared.TableData {
|
||||
bestScore := 0
|
||||
// Check each cell in the row and take the best score
|
||||
@@ -278,17 +278,17 @@ func (m *TableDataModel) filterData() {
|
||||
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 {
|
||||
@@ -307,65 +307,65 @@ 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
|
||||
}
|
||||
|
||||
@@ -409,18 +409,18 @@ func (m *TableDataModel) View() string {
|
||||
content.WriteString("\n")
|
||||
|
||||
// Show data rows with scrolling within current page
|
||||
visibleCount := Max(1, m.Shared.Height-10)
|
||||
visibleCount := max(1, m.Shared.Height-10)
|
||||
totalRows := len(m.Shared.FilteredData)
|
||||
startIdx := 0
|
||||
|
||||
|
||||
// If there are more rows than can fit on screen, scroll the view
|
||||
if totalRows > visibleCount && m.selectedRow >= visibleCount {
|
||||
startIdx = m.selectedRow - visibleCount + 1
|
||||
// Ensure we don't scroll past the end
|
||||
startIdx = min(startIdx, totalRows-visibleCount)
|
||||
}
|
||||
|
||||
endIdx := Min(totalRows, startIdx+visibleCount)
|
||||
|
||||
endIdx := min(totalRows, startIdx+visibleCount)
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
row := m.Shared.FilteredData[i]
|
||||
@@ -453,4 +453,4 @@ func (m *TableDataModel) View() string {
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,4 @@ func (k TableDataKeyMap) FullHelp() [][]key.Binding {
|
||||
{k.Enter, k.Search, k.Escape, k.Back},
|
||||
{k.GoToStart, k.GoToEnd, k.Refresh, k.SQLMode, k.ToggleHelp},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type TableListModel struct {
|
||||
@@ -254,22 +254,22 @@ func (m *TableListModel) filterTables() {
|
||||
name string
|
||||
score int
|
||||
}
|
||||
|
||||
|
||||
var matches []tableMatch
|
||||
searchLower := strings.ToLower(searchValue)
|
||||
|
||||
|
||||
for _, table := range m.Shared.Tables {
|
||||
score := m.fuzzyScore(strings.ToLower(table), searchLower)
|
||||
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 {
|
||||
@@ -289,65 +289,65 @@ 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
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ func (m *TableListModel) getVisibleCount() int {
|
||||
if m.searching {
|
||||
reservedLines += 2
|
||||
}
|
||||
return Max(1, m.Shared.Height-reservedLines)
|
||||
return max(1, m.Shared.Height-reservedLines)
|
||||
}
|
||||
|
||||
func (m *TableListModel) adjustPage() {
|
||||
@@ -389,7 +389,7 @@ func (m *TableListModel) View() string {
|
||||
} else {
|
||||
visibleCount := m.getVisibleCount()
|
||||
startIdx := m.currentPage * visibleCount
|
||||
endIdx := Min(startIdx+visibleCount, len(m.Shared.FilteredTables))
|
||||
endIdx := min(startIdx+visibleCount, len(m.Shared.FilteredTables))
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
table := m.Shared.FilteredTables[i]
|
||||
@@ -419,4 +419,4 @@ func (m *TableListModel) View() string {
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,4 +87,4 @@ func (k TableListKeyMap) FullHelp() [][]key.Binding {
|
||||
{k.Enter, k.Search, k.Escape, k.Refresh},
|
||||
{k.GoToStart, k.GoToEnd, k.SQLMode, k.ToggleHelp},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user