all repos

clerk @ c2e8832

missing tooling for ledger/hledger
4 files changed, 20 insertions(+), 35 deletions(-)
perf: lsp: index transactions by date for the date hover

countTransactionsOnDate scanned every transaction per hover request
(8.3us vs ~1us for other hovers on the 1ktxns fixture); a per-date count
map, keyed by formatDate (ast.Date embeds a Span, so it cannot be a map
key), turns the scan into a lookup.
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-15 18:59:46 +0300
Authored at: 2026-08-14 12:36:25 +0300
Change ID: ysmnotqokxkwmrnwwtvvnskzpzynytpw
Parent: 83b23d2
M internal/analyzer/analyzer.go
···
        35
        35
         	Dates       []ast.Date // unique transaction dats in sorted order

      
        36
        36
         	DateStrings []string   // same order as Dates, "YYYY-MM-DD"

      
        37
        37
         

      
        38
        
        -	// TransactionsByKey groups transactions by [TxDuplicateKey] signature.

      
        39
        
        -	TransactionsByKey map[string][]*ast.Transaction

      
        
        38
        +	TransactionsCountByDate map[string]int                // counts transactions per date, keyed by [formatDate]

      
        
        39
        +	TransactionsByKey       map[string][]*ast.Transaction // groups transactions by [TxDuplicateKey] signature

      
        40
        40
         }

      
        41
        41
         

      
        42
        42
         type AccountInfo struct {

      
M internal/analyzer/build.go
···
        17
        17
         	}

      
        18
        18
         

      
        19
        19
         	a := &Analysis{

      
        20
        
        -		Files:             rj.Occurrences,

      
        21
        
        -		Accounts:          make(map[string]*AccountInfo),

      
        22
        
        -		AccountAliases:    make(map[string]string),

      
        23
        
        -		Commodities:       make(map[string]*CommodityInfo),

      
        24
        
        -		Payees:            make(map[string]*PayeeInfo),

      
        25
        
        -		Tags:              make(map[string]*TagInfo),

      
        26
        
        -		AccountsByPrefix:  make(map[string][]string),

      
        27
        
        -		PayeeTemplates:    make(map[string][]PostingTemplate),

      
        28
        
        -		TransactionsByKey: make(map[string][]*ast.Transaction),

      
        
        20
        +		Files:                   rj.Occurrences,

      
        
        21
        +		Accounts:                make(map[string]*AccountInfo),

      
        
        22
        +		AccountAliases:          make(map[string]string),

      
        
        23
        +		Commodities:             make(map[string]*CommodityInfo),

      
        
        24
        +		Payees:                  make(map[string]*PayeeInfo),

      
        
        25
        +		Tags:                    make(map[string]*TagInfo),

      
        
        26
        +		AccountsByPrefix:        make(map[string][]string),

      
        
        27
        +		PayeeTemplates:          make(map[string][]PostingTemplate),

      
        
        28
        +		TransactionsByKey:       make(map[string][]*ast.Transaction),

      
        
        29
        +		TransactionsCountByDate: make(map[string]int),

      
        29
        30
         	}

      
        30
        31
         	for _, item := range rj.Items {

      
        31
        32
         		if item.IsInclude {

      ···
        81
        82
         		}

      
        82
        83
         		key := TxDuplicateKey(e)

      
        83
        84
         		a.TransactionsByKey[key] = append(a.TransactionsByKey[key], e)

      
        
        85
        +		a.TransactionsCountByDate[formatDate(e.Date)]++

      
        84
        86
         	case *ast.PeriodicTransaction:

      
        85
        87
         		a.PeriodicTransactions = append(a.PeriodicTransactions, e)

      
        86
        88
         		a.addPostings(fileIndex, e.Postings, nil)

      ···
        299
        301
         	}

      
        300
        302
         	sort.Strings(out)

      
        301
        303
         	return out

      
        
        304
        +}

      
        
        305
        +

      
        
        306
        +// CountTransactionsOnDate returns the number of transactions on d.

      
        
        307
        +func (a *Analysis) CountTransactionsOnDate(d ast.Date) int {

      
        
        308
        +	return a.TransactionsCountByDate[formatDate(d)]

      
        302
        309
         }

      
        303
        310
         

      
        304
        311
         func formatDate(d ast.Date) string {

      
M internal/lsp/textdocument_hover.go
···
        321
        321
         	if tx.Payee != nil {

      
        322
        322
         		fmt.Fprintf(&sb, "\n\n**Payee:** %s", tx.Payee.Name)

      
        323
        323
         	}

      
        324
        
        -	fmt.Fprintf(&sb, "\n\n**Transactions:** %d", countTransactionsOnDate(an, tx.Date))

      
        
        324
        +	fmt.Fprintf(&sb, "\n\n**Transactions:** %d", an.CountTransactionsOnDate(tx.Date))

      
        325
        325
         	fmt.Fprintf(&sb, "\n\n**Postings:** %d", len(tx.Postings))

      
        326
        326
         	return sb.String()

      
        327
        
        -}

      
        328
        
        -

      
        329
        
        -func countTransactionsOnDate(an *analyzer.Analysis, d ast.Date) int {

      
        330
        
        -	count := 0

      
        331
        
        -	for _, tx := range an.Transactions {

      
        332
        
        -		if tx.Date.Compare(d) == 0 {

      
        333
        
        -			count++

      
        334
        
        -		}

      
        335
        
        -	}

      
        336
        
        -	return count

      
        337
        327
         }

      
        338
        328
         

      
        339
        329
         func writeDateSection(sb *strings.Builder, label string, d ast.Date) {

      
M internal/lsp/textdocument_hover_test.go
···
        10
        10
         	"go.lsp.dev/protocol"

      
        11
        11
         	"go.lsp.dev/uri"

      
        12
        12
         

      
        13
        
        -	"olexsmir.xyz/clerk/internal/analyzer"

      
        14
        13
         	"olexsmir.xyz/clerk/internal/lsp/lsputil"

      
        15
        14
         	"olexsmir.xyz/clerk/internal/testutil/golden"

      
        16
        15
         	"olexsmir.xyz/clerk/journal/ast"

      ···
        44
        43
         	}

      
        45
        44
         }

      
        46
        45
         

      
        47
        
        -func TestCountTransactionsOnDate(t *testing.T) {

      
        48
        
        -	tx := func(y, m, d int) *ast.Transaction { return &ast.Transaction{Date: ast.Date{Year: y, Month: m, Day: d}} }

      
        49
        
        -	an := &analyzer.Analysis{Transactions: []*ast.Transaction{tx(2024, 1, 15), tx(2024, 1, 15), tx(2024, 2, 1)}}

      
        50
        
        -	if got := countTransactionsOnDate(an, ast.Date{Year: 2024, Month: 1, Day: 15}); got != 2 {

      
        51
        
        -		t.Errorf("got %d, want 2", got)

      
        52
        
        -	}

      
        53
        
        -	if got := countTransactionsOnDate(an, ast.Date{Year: 2025, Month: 1, Day: 15}); got != 0 {

      
        54
        
        -		t.Errorf("got %d, want 0", got)

      
        55
        
        -	}

      
        56
        
        -}

      
        57
        
        -

      
        58
        
        -func TestServerHover_DocumentNotFound(t *testing.T) {

      
        
        46
        +func TestServer_Hover_DocumentNotFound(t *testing.T) {

      
        59
        47
         	srv := NewServer("test")

      
        60
        48
         	res, err := srv.server.Hover(context.Background(), &protocol.HoverParams{

      
        61
        49
         		TextDocumentPositionParams: protocol.TextDocumentPositionParams{