all repos

clerk @ 33b67bd

missing tooling for ledger/hledger
10 files changed, 354 insertions(+), 213 deletions(-)
lsp: add bechmarks, consisten golden tests naming
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-13 13:40:52 +0300
Authored at: 2026-08-13 12:01:44 +0300
Change ID: lkysvlpsntuvvqnqotnmoyukulpotynv
Parent: 4b5c1da
A internal/lsp/testdata/completion-contexts.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +account expenses:food

      
        
        3
        +account expenses:taxi

      
        
        4
        +account assets:cash

      
        
        5
        +

      
        
        6
        +payee Grocery Store

      
        
        7
        +payee Coffee Shop

      
        
        8
        +

      
        
        9
        +commodity USD

      
        
        10
        +

      
        
        11
        +tag client

      
        
        12
        +tag project

      
        
        13
        +

      
        
        14
        +2024-01-01 Grocery Store

      
        
        15
        +    ; client:acme

      
        
        16
        +    ; project:alpha

      
        
        17
        +    expenses:food  $50

      
        
        18
        +    expenses:taxi  $30

      
        
        19
        +    assets:cash

      
        
        20
        +

      
        
        21
        +^

      
        
        22
        +2024-01-02 *

      
        
        23
        +    ^

      
        
        24
        +    expenses:^food  $50

      
        
        25
        +    expenses:food  $^50

      
        
        26
        +    expenses:food  ^

      
        
        27
        +    expenses:food  U^SD

      
        
        28
        +    expenses:food  $5^0

      
        
        29
        +    expenses:food  $50 @^ 1.5

      
        
        30
        +    * expenses:f^ood  $50

      
        
        31
        +    (expenses:f^ood)  $50

      
        
        32
        +    expenses:food  ; client:^x

      
        
        33
        +    expenses:food  ; client: ac^me

      
        
        34
        +    ; a:b, client:ac^

      
        
        35
        +    ; url:https://exa^mple

      
        
        36
        +    ; a:b:c, cli^ent:x

      
        
        37
        +    ; groc^eries

      
        
        38
        +

      
        
        39
        +2024-01-03 * (123) gro^cer

      
        
        40
        +2024-01-03=2024-01-04 cof^fee

      
        
        41
        +2024-01-03 "gro^cery"

      
        
        42
        +2024-01-03 acme | no^te

      
        
        43
        +2024-01-03 acme|note x^y

      
        
        44
        +2024-01-03 ; cli^

      
        
        45
        +2024-01-03^

      
        
        46
        +

      
        
        47
        +account expenses:food

      
        
        48
        +    no^te ignore

      
        
        49
        +

      
        
        50
        +account expenses:food

      
        
        51
        +    note: x

      
        
        52
        +

      
        
        53
        +    no^te foo

      
        
        54
        +

      
        
        55
        +account expenses:taxi

      
        
        56
        +    note: x

      
        
        57
        +   

      
        
        58
        +    no^te foo

      
        
        59
        +

      
        
        60
        +~ monthly^ budget

      
        
        61
        +

      
        
        62
        +

      
        
        63
        +2024-01-15 Grocery

      
        
        64
        +    expenses:food  $50

      
        
        65
        +    expenses:taxi  $30

      
        
        66
        +    assets:cash

      
        
        67
        +

      
        
        68
        +2024-01-16 ^

      
        
        69
        +    expenses:f^ood  $5

      
        
        70
        +    assets:cash

      
        
        71
        +

      
        
        72
        +-- expect --

      
        
        73
        +19:0 directive ""

      
        
        74
        +  C

      
        
        75
        +  D

      
        
        76
        +  N

      
        
        77
        +  P

      
        
        78
        +  Y

      
        
        79
        +  account

      
        
        80
        +  alias

      
        
        81
        +  apply

      
        
        82
        +  commodity

      
        
        83
        +  decimal-mark

      
        
        84
        +  end

      
        
        85
        +  include

      
        
        86
        +  payee

      
        
        87
        +  tag

      
        
        88
        +  year

      
        
        89
        +21:4 account ""

      
        
        90
        +  expenses:food

      
        
        91
        +  assets:cash

      
        
        92
        +  expenses:taxi

      
        
        93
        +22:13 account "expenses:"

      
        
        94
        +  expenses:food

      
        
        95
        +  expenses:taxi

      
        
        96
        +23:20 commodity "$"

      
        
        97
        +  $

      
        
        98
        +24:19 commodity ""

      
        
        99
        +  $

      
        
        100
        +  USD

      
        
        101
        +25:20 commodity "U"

      
        
        102
        +  USD

      
        
        103
        +26:21 none ""

      
        
        104
        +27:24 none ""

      
        
        105
        +28:16 account "expenses:f"

      
        
        106
        +  expenses:food

      
        
        107
        +29:15 account "expenses:f"

      
        
        108
        +  expenses:food

      
        
        109
        +30:28 tag-value ""

      
        
        110
        +  ac

      
        
        111
        +  acme

      
        
        112
        +  x

      
        
        113
        +31:31 tag-value "ac"

      
        
        114
        +  ac

      
        
        115
        +  acme

      
        
        116
        +32:20 tag-value "ac"

      
        
        117
        +  ac

      
        
        118
        +  acme

      
        
        119
        +33:21 tag-value "https://exa"

      
        
        120
        +  https://example

      
        
        121
        +34:16 tag "cli"

      
        
        122
        +  client

      
        
        123
        +35:10 tag "groc"

      
        
        124
        +37:22 payee "gro"

      
        
        125
        +  grocer

      
        
        126
        +  grocery

      
        
        127
        +  Grocery

      
        
        128
        +  Grocery Store

      
        
        129
        +38:25 payee "cof"

      
        
        130
        +  coffee

      
        
        131
        +  Coffee Shop

      
        
        132
        +39:15 payee "gro"

      
        
        133
        +  grocer

      
        
        134
        +  grocery

      
        
        135
        +  Grocery

      
        
        136
        +  Grocery Store

      
        
        137
        +40:20 none ""

      
        
        138
        +41:22 none ""

      
        
        139
        +42:16 tag "cli"

      
        
        140
        +  client

      
        
        141
        +43:10 payee ""

      
        
        142
        +  Grocery

      
        
        143
        +  Grocery Store

      
        
        144
        +  acme

      
        
        145
        +  acme|note xy

      
        
        146
        +  coffee

      
        
        147
        +  grocer

      
        
        148
        +  grocery

      
        
        149
        +  Coffee Shop

      
        
        150
        +46:6 none ""

      
        
        151
        +51:6 account "no"

      
        
        152
        +  expenses:food

      
        
        153
        +56:6 none ""

      
        
        154
        +58:9 none ""

      
        
        155
        +66:11 payee ""

      
        
        156
        +  Grocery

      
        
        157
        +  Grocery Store

      
        
        158
        +  acme

      
        
        159
        +  acme|note xy

      
        
        160
        +  coffee

      
        
        161
        +  grocer

      
        
        162
        +  grocery

      
        
        163
        +  Coffee Shop

      
        
        164
        +67:14 account "expenses:f"

      
        
        165
        +  expenses:food

      
D internal/lsp/testdata/completion-crlf.txtar
···
        1
        
        --- in.journal --

      
        2
        
        -2024-01-15 Grocery

      
        3
        
        -    expenses:food  $50

      
        4
        
        -    expenses:taxi  $30

      
        5
        
        -    assets:cash

      
        6
        
        -

      
        7
        
        -2024-01-16 ^

      
        8
        
        -    expenses:f^ood  $5

      
        9
        
        -    assets:cash

      
        10
        
        --- expect --

      
        11
        
        -5:11 payee ""

      
        12
        
        -  Grocery

      
        13
        
        -6:14 account "expenses:f"

      
        14
        
        -  expenses:food

      
M internal/lsp/testdata/completion-journal.txtar
···
        40
        40
         

      
        41
        41
         acc^ount

      
        42
        42
         

      
        
        43
        +

      
        
        44
        +2024-01-15 Супермаркет

      
        
        45
        +  Витрати:Продукти  ¥50

      
        
        46
        +  Витрати:Транспорт  ¥30

      
        
        47
        +  Активи:Готівка

      
        
        48
        +

      
        
        49
        +2024-01-15 Продукти

      
        
        50
        +  Витрати:Продукти  €10

      
        
        51
        +  Активи:Готівка

      
        
        52
        +

      
        
        53
        +2024-01-16 Супер^маркет

      
        
        54
        +  Витрати:Про^дукти  ¥20

      
        
        55
        +  Активи:Готівка

      
        
        56
        +

      
        
        57
        +2024-01-16 Прод^укти

      
        
        58
        +  Витрати:Тр^анспорт  €5

      
        
        59
        +  Активи:Готівка

      
        
        60
        +

      
        
        61
        +commodity ¥^元

      
        
        62
        +

      
        
        63
        +tag 客^户

      
        
        64
        +

      
        
        65
        +

      
        43
        66
         -- expect --

      
        44
        67
         19:10 tag-value "a"

      
        45
        68
           acme

      ···
        47
        70
           alpha

      
        48
        71
         22:11 payee ""

      
        49
        72
           Grocery

      
        
        73
        +  Продукти

      
        
        74
        +  Супермаркет

      
        50
        75
           Store

      
        51
        76
         23:12 account "expenses:f"

      
        52
        77
           expenses:food

      ···
        64
        89
           project

      
        65
        90
         39:3 directive "acc"

      
        66
        91
           account

      
        
        92
        +51:16 payee "Супер"

      
        
        93
        +  Супермаркет

      
        
        94
        +52:13 account "Витрати:Про"

      
        
        95
        +  Витрати:Продукти

      
        
        96
        +55:15 payee "Прод"

      
        
        97
        +  Продукти

      
        
        98
        +56:12 account "Витрати:Тр"

      
        
        99
        +  Витрати:Транспорт

      
        
        100
        +59:11 commodity "¥"

      
        
        101
        +  ¥

      
        
        102
        +61:5 tag "客"

      
        
        103
        +  客户

      
A internal/lsp/testdata/completion-no-transactions.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +account expenses:food

      
        
        3
        +account expenses:fo^od

      
        
        4
        +

      
        
        5
        +-- expect --

      
        
        6
        +1:19 account "expenses:fo"

      
        
        7
        +  expenses:food

      
D internal/lsp/testdata/completion-unicode.txtar
···
        1
        
        --- in.journal --

      
        2
        
        -2024-01-15 Супермаркет

      
        3
        
        -  Витрати:Продукти  ¥50

      
        4
        
        -  Витрати:Транспорт  ¥30

      
        5
        
        -  Активи:Готівка

      
        6
        
        -

      
        7
        
        -2024-01-15 Продукти

      
        8
        
        -  Витрати:Продукти  €10

      
        9
        
        -  Активи:Готівка

      
        10
        
        -

      
        11
        
        -2024-01-16 Супер^маркет

      
        12
        
        -  Витрати:Про^дукти  ¥20

      
        13
        
        -  Активи:Готівка

      
        14
        
        -

      
        15
        
        -2024-01-16 Прод^укти

      
        16
        
        -  Витрати:Тр^анспорт  €5

      
        17
        
        -  Активи:Готівка

      
        18
        
        -

      
        19
        
        -commodity ¥^元

      
        20
        
        -

      
        21
        
        -tag 客^户

      
        22
        
        -

      
        23
        
        --- expect --

      
        24
        
        -9:16 payee "Супер"

      
        25
        
        -  Супермаркет

      
        26
        
        -10:13 account "Витрати:Про"

      
        27
        
        -  Витрати:Продукти

      
        28
        
        -13:15 payee "Прод"

      
        29
        
        -  Продукти

      
        30
        
        -14:12 account "Витрати:Тр"

      
        31
        
        -  Витрати:Транспорт

      
        32
        
        -17:11 commodity "¥"

      
        33
        
        -  ¥

      
        34
        
        -19:5 tag "客"

      
        35
        
        -  客户

      
M internal/lsp/textdocument_completion_test.go
···
        15
        15
         	"olexsmir.xyz/clerk/journal"

      
        16
        16
         )

      
        17
        17
         

      
        18
        
        -func TestDetectCompletionCtx(t *testing.T) {

      
        19
        
        -	tests := map[string]struct {

      
        20
        
        -		ctx      cmplCtx

      
        21
        
        -		in, want string

      
        22
        
        -	}{

      
        23
        
        -		"posting account":                  {cmplAccount, "    expenses:f^ood  $50\n", "expenses:f"},

      
        24
        
        -		"posting account empty":            {cmplAccount, "    ^\n", ""},

      
        25
        
        -		"posting account after colon":      {cmplAccount, "    expenses:^food  $50\n", "expenses:"},

      
        26
        
        -		"posting amount commodity":         {cmplCommodity, "    expenses:food  $^50\n", "$"},

      
        27
        
        -		"posting empty amount region":      {cmplCommodity, "    expenses:food  ^\n", ""},

      
        28
        
        -		"posting commodity word":           {cmplCommodity, "    expenses:food  U^SD\n", "U"},

      
        29
        
        -		"posting amount number":            {cmplNone, "    expenses:food  $5^0\n", ""},

      
        30
        
        -		"posting cost quantity":            {cmplNone, "    expenses:food  $50 @^ 1.5\n", ""},

      
        31
        
        -		"posting status":                   {cmplAccount, "    * expenses:f^ood  $50\n", "expenses:f"},

      
        32
        
        -		"posting virtual":                  {cmplAccount, "    (expenses:f^ood)  $50\n", "expenses:f"},

      
        33
        
        -		"posting comment tag":              {cmplTagName, "    expenses:food  ; clie^nt:x\n", "clie"},

      
        34
        
        -		"posting comment tag value":        {cmplTagValue, "    expenses:food  ; client:^x\n", ""},

      
        35
        
        -		"header payee":                     {cmplPayee, "2024-01-15 acm^e\n    assets:cash\n", "acm"},

      
        36
        
        -		"header payee empty":               {cmplPayee, "2024-01-15 ^\n", ""},

      
        37
        
        -		"header payee right after date":    {cmplPayee, "2024-01-15^\n", ""},

      
        38
        
        -		"header status and code":           {cmplPayee, "2024-01-15 * (123) gro^cer\n", "gro"},

      
        39
        
        -		"header second date":               {cmplPayee, "2024-01-15=2024-01-16 acm^e\n", "acm"},

      
        40
        
        -		"header quoted payee":              {cmplPayee, "2024-01-15 \"ac^me\"\n", "ac"},

      
        41
        
        -		"header pipe note":                 {cmplNone, "2024-01-15 acme | no^te\n", ""},

      
        42
        
        -		"header pipe inline":               {cmplNone, "2024-01-15 acme|note x^y\n", ""},

      
        43
        
        -		"header inline comment":            {cmplTagName, "2024-01-15 ; foo^", "foo"},

      
        44
        
        -		"directive keyword partial":        {cmplDirective, "acc^ount expenses\n", "acc"},

      
        45
        
        -		"directive keyword empty line":     {cmplDirective, "\n^", ""},

      
        46
        
        -		"account directive value":          {cmplAccount, "account exp^enses\n", "exp"},

      
        47
        
        -		"commodity directive value":        {cmplCommodity, "commodity U^SD\n", "U"},

      
        48
        
        -		"payee directive value":            {cmplPayee, "payee ac^me\n", "ac"},

      
        49
        
        -		"tag directive value":              {cmplTagName, "tag pro^ject\n", "pro"},

      
        50
        
        -		"comment tag":                      {cmplTagName, "; clie^nt:x\n", "clie"},

      
        51
        
        -		"comment value":                    {cmplTagValue, "; client:x^yz\n", "x"},

      
        52
        
        -		"comment value empty":              {cmplTagValue, "; client:^\n", ""},

      
        53
        
        -		"comment value after space":        {cmplTagValue, "; client: ac^me\n", "ac"},

      
        54
        
        -		"comment value second tag":         {cmplTagValue, "; a:b, client:x^\n", "x"},

      
        55
        
        -		"comment value with colon":         {cmplTagValue, "; url:https://example^\n", "https://example"},

      
        56
        
        -		"comment key after previous value": {cmplTagName, "; a:b:c, cli^ent:x\n", "cli"},

      
        57
        
        -		"comment plain text":               {cmplTagName, "; groc^eries\n", "groc"},

      
        58
        
        -		"subdirective ignored":             {cmplNone, "account expenses\n    no^te ignore\n", ""},

      
        59
        
        -		"periodic header":                  {cmplNone, "~ monthly^ budget\n", ""},

      
        60
        
        -		"cjk posting":                      {cmplAccount, "    支出:食^物  50\n", "支出:食"},

      
        61
        
        -		"ukrainian payee":                  {cmplPayee, "2024-01-15 прод^укти\n", "прод"},

      
        62
        
        -		"crlf posting":                     {cmplAccount, "2024-01-15 x\r\n    expenses:f^ood  $50\r\n", "expenses:f"},

      
        63
        
        -		"crlf header":                      {cmplPayee, "2024-01-15 acm^e\r\n    assets:cash\r\n", "acm"},

      
        64
        
        -	}

      
        65
        
        -	for tname, tt := range tests {

      
        66
        
        -		t.Run(tname, func(t *testing.T) {

      
        67
        
        -			cont := tt.in

      
        68
        
        -			i := strings.Index(cont, "^")

      
        69
        
        -			if i < 0 {

      
        70
        
        -				t.Fatal("no cursor marker '^' in content")

      
        71
        
        -			}

      
        72
        
        -

      
        73
        
        -			cont = cont[:i] + cont[i+1:]

      
        74
        
        -			ctx, start := detectCompletionCtx(cont, i)

      
        75
        
        -			prefix := cont[start:i]

      
        76
        
        -

      
        77
        
        -			if ctx != tt.ctx {

      
        78
        
        -				t.Errorf("ctx = %v, want %v", ctx, tt.ctx)

      
        79
        
        -			}

      
        80
        
        -			if prefix != tt.want {

      
        81
        
        -				t.Errorf("prefix = %q, want %q", prefix, tt.want)

      
        82
        
        -			}

      
        83
        
        -		})

      
        84
        
        -	}

      
        85
        
        -}

      
        86
        
        -

      
        87
        
        -func (c cmplCtx) String() string {

      
        88
        
        -	switch c {

      
        89
        
        -	case cmplNone:

      
        90
        
        -		return "none"

      
        91
        
        -	case cmplAccount:

      
        92
        
        -		return "account"

      
        93
        
        -	case cmplPayee:

      
        94
        
        -		return "payee"

      
        95
        
        -	case cmplCommodity:

      
        96
        
        -		return "commodity"

      
        97
        
        -	case cmplTagName:

      
        98
        
        -		return "tag"

      
        99
        
        -	case cmplTagValue:

      
        100
        
        -		return "tag-value"

      
        101
        
        -	case cmplDirective:

      
        102
        
        -		return "directive"

      
        103
        
        -	default:

      
        104
        
        -		return "?"

      
        105
        
        -	}

      
        106
        
        -}

      
        107
        
        -

      
        108
        18
         func TestDetectCompletion_NoJournal(t *testing.T) {

      
        109
        19
         	// detection must not depend on a parse

      
        110
        20
         	i := strings.Index("    no^te foo\n", "^")

      ···
        115
        25
         	}

      
        116
        26
         }

      
        117
        27
         

      
        118
        
        -func TestDetectCompletion_Subdirective(t *testing.T) {

      
        119
        
        -	// the lexical rule suppresses completion on subdirective lines without a parse

      
        120
        
        -	i := strings.Index("account expenses\n    no^te ignore\n", "^")

      
        121
        
        -	content := "account expenses\n    note ignore\n"

      
        122
        
        -	if ctx, _ := detectCompletionCtx(content, i); ctx != cmplNone {

      
        123
        
        -		t.Errorf("subdirective: ctx = %v, want none", ctx)

      
        124
        
        -	}

      
        125
        
        -	// a posting line after a transaction header is still an account context

      
        126
        
        -	i = strings.Index("2024-01-15 acme\n    expe^nses:food  $50\n", "^")

      
        127
        
        -	content = "2024-01-15 acme\n    expenses:food  $50\n"

      
        128
        
        -	if ctx, _ := detectCompletionCtx(content, i); ctx != cmplAccount {

      
        129
        
        -		t.Errorf("posting: ctx = %v, want account", ctx)

      
        130
        
        -	}

      
        131
        
        -	// a blank line ends the directive body; a whitespace-only line does not

      
        132
        
        -	i = strings.Index("account expenses\n    note: x\n\n    no^te\n", "^")

      
        133
        
        -	content = "account expenses\n    note: x\n\n    note\n"

      
        134
        
        -	if ctx, _ := detectCompletionCtx(content, i); ctx != cmplAccount {

      
        135
        
        -		t.Errorf("after blank line: ctx = %v, want account", ctx)

      
        136
        
        -	}

      
        137
        
        -	i = strings.Index("account expenses\n    note: x\n   \n    no^te\n", "^")

      
        138
        
        -	content = "account expenses\n    note: x\n   \n    note\n"

      
        139
        
        -	if ctx, _ := detectCompletionCtx(content, i); ctx != cmplNone {

      
        140
        
        -		t.Errorf("whitespace-only line keeps body: ctx = %v, want none", ctx)

      
        141
        
        -	}

      
        142
        
        -}

      
        143
        
        -

      
        144
        
        -func TestCompleteItems_NoTransactions(t *testing.T) {

      
        145
        
        -	// directives-only doc: empty a.Dates must not panic the ranking

      
        146
        
        -	content := "account expenses:food\n\n^"

      
        147
        
        -	i := strings.Index(content, "^")

      
        148
        
        -	content = content[:i]

      
        149
        
        -	a := analyzer.Build(journal.NewLoader().ResolveBytes("", []byte(content)))

      
        150
        
        -	if len(a.Dates) != 0 {

      
        151
        
        -		t.Fatalf("setup: want 0 dates, got %d", len(a.Dates))

      
        152
        
        -	}

      
        153
        
        -	items := cmplItems(a, cmplAccount, content, len(content), len(content))

      
        154
        
        -	if len(items) == 0 {

      
        155
        
        -		t.Fatal("expected the directive-defined account to complete")

      
        156
        
        -	}

      
        157
        
        -	if items[0].Label != "expenses:food" {

      
        158
        
        -		t.Errorf("label = %q, want expenses:food", items[0].Label)

      
        159
        
        -	}

      
        160
        
        -}

      
        161
        
        -

      
        162
        
        -// Golden

      
        163
        
        -

      
        164
        
        -func TestCompletionTxtar(t *testing.T) {

      
        165
        
        -	for _, tt := range []string{"completion-journal", "completion-unicode", "completion-crlf"} {

      
        
        28
        +func TestGolden_Completion(t *testing.T) {

      
        
        29
        +	for _, tt := range []string{"completion-contexts", "completion-journal", "completion-no-transactions"} {

      
        166
        30
         		ar := golden.Read(t, tt)

      
        167
        31
         

      
        168
        32
         		t.Run(tt, func(t *testing.T) {

      ···
        192
        56
         	}

      
        193
        57
         }

      
        194
        58
         

      
        195
        
        -// Benchmark

      
        
        59
        +func (c cmplCtx) String() string {

      
        
        60
        +	switch c {

      
        
        61
        +	case cmplNone:

      
        
        62
        +		return "none"

      
        
        63
        +	case cmplAccount:

      
        
        64
        +		return "account"

      
        
        65
        +	case cmplPayee:

      
        
        66
        +		return "payee"

      
        
        67
        +	case cmplCommodity:

      
        
        68
        +		return "commodity"

      
        
        69
        +	case cmplTagName:

      
        
        70
        +		return "tag"

      
        
        71
        +	case cmplTagValue:

      
        
        72
        +		return "tag-value"

      
        
        73
        +	case cmplDirective:

      
        
        74
        +		return "directive"

      
        
        75
        +	default:

      
        
        76
        +		return "?"

      
        
        77
        +	}

      
        
        78
        +}

      
        196
        79
         

      
        197
        80
         func BenchmarkCompletion(b *testing.B) {

      
        198
        81
         	path := "../../journal/testdata/journals/actual-1ktxns-100accts.journal"

      ···
        207
        90
         	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")

      
        208
        91
         	srv.server.current = a

      
        209
        92
         

      
        210
        
        -	for _, tc := range []struct {

      
        211
        
        -		name string

      
        212
        
        -		pos  int

      
        213
        
        -	}{

      
        214
        
        -		{"1k txns, account", strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2},

      
        215
        
        -		{"1k txns, empty payee", strings.Index(content, "transaction 1") + len("transaction ")},

      
        216
        
        -		{"1k txns, commodity", strings.Index(content, "2 B @@") + len("2 B")},

      
        
        93
        +	for tname, tt := range map[string]int{

      
        
        94
        +		"1k txns, account":     strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2,

      
        
        95
        +		"1k txns, empty payee": strings.Index(content, "transaction 1") + len("transaction "),

      
        
        96
        +		"1k txns, commodity":   strings.Index(content, "2 B @@") + len("2 B"),

      
        217
        97
         	} {

      
        218
        
        -		b.Run(tc.name, func(b *testing.B) {

      
        219
        
        -			line, col := lsputil.LineCol(content, tc.pos)

      
        
        98
        +		b.Run(tname, func(b *testing.B) {

      
        
        99
        +			line, col := lsputil.LineCol(content, tt)

      
        220
        100
         			params := &protocol.CompletionParams{

      
        221
        101
         				TextDocumentPositionParams: protocol.TextDocumentPositionParams{

      
        222
        102
         					TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},

      
        223
        103
         					Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},

      
        224
        104
         				},

      
        225
        105
         			}

      
        226
        
        -			// warm up: first request parses the journal lazily

      
        
        106
        +

      
        
        107
        +			// warm up

      
        227
        108
         			if _, err := srv.server.Completion(b.Context(), params); err != nil {

      
        228
        109
         				b.Fatal(err)

      
        229
        110
         			}

      
M internal/lsp/textdocument_definition_test.go
···
        9
        9
         	"go.lsp.dev/protocol"

      
        10
        10
         	"go.lsp.dev/uri"

      
        11
        11
         

      
        
        12
        +	"olexsmir.xyz/clerk/internal/analyzer"

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

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

      
        
        15
        +	"olexsmir.xyz/clerk/journal"

      
        13
        16
         )

      
        14
        17
         

      
        15
        18
         func TestDefinition_DocumentNotFound(t *testing.T) {

      ···
        28
        31
         	}

      
        29
        32
         }

      
        30
        33
         

      
        31
        
        -// Golden

      
        32
        
        -

      
        33
        
        -func TestDefinitionTxtar(t *testing.T) {

      
        
        34
        +func TestGolden_Definition(t *testing.T) {

      
        34
        35
         	for _, tt := range []string{"definition-journal", "definition-include"} {

      
        35
        36
         		ar := golden.Read(t, tt)

      
        36
        37
         		t.Run(tt, func(t *testing.T) {

      ···
        59
        60
         		})

      
        60
        61
         	}

      
        61
        62
         }

      
        
        63
        +

      
        
        64
        +func BenchmarkDefinition(b *testing.B) {

      
        
        65
        +	path := "../../journal/testdata/journals/actual-1ktxns-100accts.journal"

      
        
        66
        +	abs, err := filepath.Abs(path)

      
        
        67
        +	if err != nil {

      
        
        68
        +		b.Fatal(err)

      
        
        69
        +	}

      
        
        70
        +	rj, err := journal.NewLoader().Resolve(abs)

      
        
        71
        +	if err != nil {

      
        
        72
        +		b.Fatal(err)

      
        
        73
        +	}

      
        
        74
        +	a := analyzer.Build(rj)

      
        
        75
        +	content := string(rj.Occurrences[0].Src)

      
        
        76
        +

      
        
        77
        +	srv := NewServer("test")

      
        
        78
        +	u := uri.File(abs)

      
        
        79
        +	srv.server.openDoc(u, content, 1, "journal")

      
        
        80
        +	srv.server.current = a

      
        
        81
        +

      
        
        82
        +	for tname, tt := range map[string]int{

      
        
        83
        +		"1k txns, account": strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2,

      
        
        84
        +		"1k txns, payee":strings.Index(content, "transaction 1") + len("transaction "),

      
        
        85
        +		"1k txns, commodity":strings.Index(content, "2 B @@") + len("2 B"),

      
        
        86
        +	} {

      
        
        87
        +		b.Run(tname, func(b *testing.B) {

      
        
        88
        +			line, col := lsputil.LineCol(content, tt)

      
        
        89
        +			params := &protocol.DefinitionParams{

      
        
        90
        +				TextDocumentPositionParams: protocol.TextDocumentPositionParams{

      
        
        91
        +					TextDocument: protocol.TextDocumentIdentifier{URI: u},

      
        
        92
        +					Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},

      
        
        93
        +				},

      
        
        94
        +			}

      
        
        95
        +			// warm up: first request resolves the symbol lazily; assert it found one

      
        
        96
        +			res, err := srv.server.Definition(b.Context(), params)

      
        
        97
        +			if err != nil {

      
        
        98
        +				b.Fatal(err)

      
        
        99
        +			}

      
        
        100
        +			locs, ok := res.(protocol.LocationSlice)

      
        
        101
        +			if !ok || len(locs) == 0 {

      
        
        102
        +				b.Fatalf("%s: no definition", tname)

      
        
        103
        +			}

      
        
        104
        +			b.ReportAllocs()

      
        
        105
        +			b.ResetTimer()

      
        
        106
        +			for b.Loop() {

      
        
        107
        +				if _, err := srv.server.Definition(b.Context(), params); err != nil {

      
        
        108
        +					b.Fatal(err)

      
        
        109
        +				}

      
        
        110
        +			}

      
        
        111
        +		})

      
        
        112
        +	}

      
        
        113
        +}

      
M internal/lsp/textdocument_hover_test.go
···
        75
        75
         

      
        76
        76
         // Golden

      
        77
        77
         

      
        78
        
        -func TestHoverTxtar(t *testing.T) {

      
        
        78
        +func TestGolden_Hover(t *testing.T) {

      
        79
        79
         	for _, tt := range []string{"hover-journal", "hover-include"} {

      
        80
        80
         		ar := golden.Read(t, tt)

      
        81
        81
         		t.Run(tt, func(t *testing.T) {

      
M internal/lsp/textdocument_rename_test.go
···
        11
        11
         	"go.lsp.dev/protocol"

      
        12
        12
         	"go.lsp.dev/uri"

      
        13
        13
         

      
        
        14
        +	"olexsmir.xyz/clerk/internal/analyzer"

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

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

      
        
        17
        +	"olexsmir.xyz/clerk/journal"

      
        16
        18
         )

      
        17
        19
         

      
        18
        20
         func TestValidateRenameName(t *testing.T) {

      ···
        69
        71
         

      
        70
        72
         // Golden

      
        71
        73
         

      
        72
        
        -func TestRenameTxtar(t *testing.T) {

      
        
        74
        +func TestGolden_Rename(t *testing.T) {

      
        73
        75
         	for _, tt := range []string{"rename-account", "rename-commodity", "rename-payee", "rename-tag"} {

      
        74
        76
         		ar := golden.Read(t, tt)

      
        75
        77
         		t.Run(tt, func(t *testing.T) {

      ···
        121
        123
         				}

      
        122
        124
         			}

      
        123
        125
         			golden.Assert(t, ar, b.String())

      
        
        126
        +		})

      
        
        127
        +	}

      
        
        128
        +}

      
        
        129
        +

      
        
        130
        +func BenchmarkRename(b *testing.B) {

      
        
        131
        +	path := "../../journal/testdata/journals/actual-1ktxns-100accts.journal"

      
        
        132
        +	abs, err := filepath.Abs(path)

      
        
        133
        +	if err != nil {

      
        
        134
        +		b.Fatal(err)

      
        
        135
        +	}

      
        
        136
        +	rj, err := journal.NewLoader().Resolve(abs)

      
        
        137
        +	if err != nil {

      
        
        138
        +		b.Fatal(err)

      
        
        139
        +	}

      
        
        140
        +	a := analyzer.Build(rj)

      
        
        141
        +	content := string(rj.Occurrences[0].Src)

      
        
        142
        +

      
        
        143
        +	srv := NewServer("test")

      
        
        144
        +	u := uri.File(abs)

      
        
        145
        +	srv.server.openDoc(u, content, 1, "journal")

      
        
        146
        +	srv.server.current = a

      
        
        147
        +

      
        
        148
        +	for tname, tt := range map[string]struct {

      
        
        149
        +		newName string

      
        
        150
        +		pos     int

      
        
        151
        +	}{

      
        
        152
        +		"1k txns, account":   {"1:2:3x", strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2},

      
        
        153
        +		"1k txns, payee":     {"transaction 1x", strings.Index(content, "transaction 1") + len("transaction ")},

      
        
        154
        +		"1k txns, commodity": {"Bx", strings.Index(content, "2 B @@") + len("2 B")},

      
        
        155
        +	} {

      
        
        156
        +		b.Run(tname, func(b *testing.B) {

      
        
        157
        +			line, col := lsputil.LineCol(content, tt.pos)

      
        
        158
        +			params := &protocol.RenameParams{

      
        
        159
        +				TextDocumentPositionParams: protocol.TextDocumentPositionParams{

      
        
        160
        +					TextDocument: protocol.TextDocumentIdentifier{URI: u},

      
        
        161
        +					Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},

      
        
        162
        +				},

      
        
        163
        +				NewName: tt.newName,

      
        
        164
        +			}

      
        
        165
        +			// warm up: first request generates the workspace edits; assert it found one

      
        
        166
        +			edit, err := srv.server.Rename(b.Context(), params)

      
        
        167
        +			if err != nil {

      
        
        168
        +				b.Fatal(err)

      
        
        169
        +			}

      
        
        170
        +			if edit == nil || len(edit.Changes) == 0 {

      
        
        171
        +				b.Fatalf("%s: no edits", tname)

      
        
        172
        +			}

      
        
        173
        +			b.ReportAllocs()

      
        
        174
        +			b.ResetTimer()

      
        
        175
        +			for b.Loop() {

      
        
        176
        +				if _, err := srv.server.Rename(b.Context(), params); err != nil {

      
        
        177
        +					b.Fatal(err)

      
        
        178
        +				}

      
        
        179
        +			}

      
        124
        180
         		})

      
        125
        181
         	}

      
        126
        182
         }

      
M internal/lsp/textdocument_semantic_test.go
···
        160
        160
         

      
        161
        161
         // Golden

      
        162
        162
         

      
        163
        
        -func TestSemanticTokensTxtar(t *testing.T) {

      
        164
        
        -	tests := []string{

      
        165
        
        -		"semantic-empty",

      
        166
        
        -		"semantic-journal",

      
        167
        
        -		"semantic-directives",

      
        168
        
        -		"semantic-unparseable",

      
        169
        
        -		"semantic-with-errors",

      
        170
        
        -	}

      
        171
        
        -

      
        172
        
        -	for _, tt := range tests {

      
        
        163
        +func TestGolden_SemanticTokens(t *testing.T) {

      
        
        164
        +	for _, tt := range []string{"semantic-empty", "semantic-journal", "semantic-directives", "semantic-unparseable", "semantic-with-errors"} {

      
        173
        165
         		ar := golden.Read(t, tt)

      
        174
        166
         

      
        175
        167
         		t.Run(tt+"_golden", func(t *testing.T) {