all repos

clerk @ 5581d44cd1b540070af709dfb8aafc68722f1195

missing tooling for ledger/hledger

clerk/internal/lsp/textdocument_completion_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: P directive completion, 1 month ago
1
// TODO:
2
package lsp
3
4
import (
5
	"fmt"
6
	"strings"
7
	"testing"
8
	"time"
9
10
	"go.lsp.dev/protocol"
11
	"go.lsp.dev/uri"
12
13
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
14
	"olexsmir.xyz/clerk/internal/testutil/golden"
15
	"olexsmir.xyz/clerk/journal"
16
)
17
18
func TestDetectCompletion_NoJournal(t *testing.T) {
19
	// detection must not depend on a parse
20
	i := strings.Index("    no^te foo\n", "^")
21
	content := "    note foo\n"
22
	ctx, _ := detectCompletionCtx(content, i)
23
	if ctx != cmplAccount {
24
		t.Errorf("ctx = %v, want account", ctx)
25
	}
26
}
27
28
func TestDateStyle(t *testing.T) {
29
	tests := map[string]struct {
30
		history []string
31
		sep     byte
32
		hasYear bool
33
	}{
34
		"empty":    {nil, '-', true},
35
		"dash":     {[]string{"2024-01-02"}, '-', true},
36
		"slash":    {[]string{"2024/01/20"}, '/', true},
37
		"yearless": {[]string{"6-10"}, '-', false},
38
	}
39
	for name, tt := range tests {
40
		t.Run(name, func(t *testing.T) {
41
			sep, hasYear := dateStyle(tt.history)
42
			if sep != tt.sep || hasYear != tt.hasYear {
43
				t.Errorf("dateStyle = %q/%v, want %q/%v", sep, hasYear, tt.sep, tt.hasYear)
44
			}
45
		})
46
	}
47
}
48
49
func TestDateTokenEnd(t *testing.T) {
50
	tests := map[string]struct {
51
		content string
52
		start   int
53
		want    int
54
	}{
55
		"full date":            {"2024-01-02 x", 0, 10},
56
		"unpadded":             {"2024/1/2 x", 0, 8},
57
		"yearless":             {"6-10 x", 0, 4},
58
		"stops at =":           {"2024-01-02=2024-01-03", 0, 10},
59
		"stops at comment":     {"2024-01-02;x", 0, 10},
60
		"partial trailing sep": {"2024- x", 0, 5},
61
		"partial mid":          {"2024-0 x", 0, 6},
62
	}
63
	for name, tt := range tests {
64
		t.Run(name, func(t *testing.T) {
65
			if got := dateTokenEnd(tt.content, tt.start); got != tt.want {
66
				t.Errorf("dateTokenEnd = %d, want %d", got, tt.want)
67
			}
68
		})
69
	}
70
}
71
72
func TestDatePatternMatch(t *testing.T) {
73
	tests := map[string]struct {
74
		pattern, canonical string
75
		want               bool
76
	}{
77
		"digit subsequence":   {"2024-01-0", "2024-01-05", true},
78
		"same sep":            {"2024/0", "2024/01/20", true},
79
		"other sep rejected":  {"2024/0", "2024-01-20", false},
80
		"yearless":            {"6-10", "6-10", true},
81
		"yearless padded rej": {"06-10", "6-10", false},
82
		"no match":            {"2025", "2024-01-20", false},
83
		"empty":               {"", "2024-01-20", true},
84
	}
85
	for name, tt := range tests {
86
		t.Run(name, func(t *testing.T) {
87
			if got := datePatternMatch(tt.pattern, tt.canonical); got != tt.want {
88
				t.Errorf("datePatternMatch = %v, want %v", got, tt.want)
89
			}
90
		})
91
	}
92
}
93
94
func TestGolden_Completion(t *testing.T) {
95
	for _, tt := range []string{"completion-contexts", "completion-journal", "completion-no-transactions"} {
96
		t.Run(tt, func(t *testing.T) {
97
			ar := golden.Read(t, tt)
98
			h := newTxtarHarness(t, ar)
99
100
			var b strings.Builder
101
			for i, c := range h.cursors {
102
				tdp := h.textDocumentPosition(i)
103
				res, err := h.srv.Completion(t.Context(), &protocol.CompletionParams{
104
					TextDocumentPositionParams: tdp,
105
				})
106
				if err != nil {
107
					t.Fatal(err)
108
				}
109
				ctx, start := detectCompletionCtx(h.content, c)
110
				fmt.Fprintf(&b, "%d:%d %s %q\n", tdp.Position.Line, tdp.Position.Character, ctx, h.content[start:c])
111
				list, ok := res.(*protocol.CompletionList)
112
				if !ok {
113
					t.Fatalf("Completion returned %T, want *protocol.CompletionList", res)
114
				}
115
				now := time.Now()
116
				var sep byte
117
				var hasYear bool
118
				if ctx == cmplDate {
119
					if an := h.srv.analysisFor(h.uri); an != nil {
120
						sep, hasYear = dateStyle(an.DateStrings)
121
					}
122
				}
123
				for _, item := range list.Items {
124
					label := item.Label
125
					if ctx == cmplDate {
126
						switch label {
127
						case renderDate(now, sep, hasYear):
128
							label = "today"
129
						case renderDate(now.AddDate(0, 0, -1), sep, hasYear):
130
							label = "yesterday"
131
						case renderDate(now.AddDate(0, 0, -2), sep, hasYear):
132
							label = "2 days ago"
133
						}
134
					}
135
					fmt.Fprintf(&b, "  %s\n", label)
136
				}
137
			}
138
			golden.Assert(t, ar, b.String())
139
		})
140
	}
141
}
142
143
func (c cmplCtx) String() string {
144
	switch c {
145
	case cmplNone:
146
		return "none"
147
	case cmplAccount:
148
		return "account"
149
	case cmplPayee:
150
		return "payee"
151
	case cmplCommodity:
152
		return "commodity"
153
	case cmplTagName:
154
		return "tag"
155
	case cmplTagValue:
156
		return "tag-value"
157
	case cmplDate:
158
		return "date"
159
	case cmplDirective:
160
		return "directive"
161
	default:
162
		return "?"
163
	}
164
}
165
166
func BenchmarkCompletion(b *testing.B) {
167
	path := "../../journal/testdata/journals/actual-1ktxns-100accts.journal"
168
	rj, err := journal.NewLoader().Resolve(path)
169
	if err != nil {
170
		b.Fatal(err)
171
	}
172
	content := string(rj.Occurrences[0].Src)
173
174
	srv := NewServer("test")
175
	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")
176
	srv.server.analysisFor(uri.URI("file:///test.journal")) // warm the per-doc cache
177
178
	for tname, tt := range map[string]int{
179
		"1k txns, account":        strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2,
180
		"1k txns, empty payee":    strings.Index(content, "transaction 1") + len("transaction "),
181
		"1k txns, commodity":      strings.Index(content, "2 B @@") + len("2 B"),
182
		"1k txns, date":           strings.Index(content, "2000-01-0") + len("2000-01-0"),
183
		"1k txns, price date":     strings.Index(content, "P 2000-01-0") + len("P 2000-01-0"),
184
		"1k txns, price quantity": strings.Index(content, "P 2000-01-01 A  0.70") + len("P 2000-01-01 A  0.70"),
185
	} {
186
		b.Run(tname, func(b *testing.B) {
187
			line, col := lsputil.LineCol(content, tt)
188
			params := &protocol.CompletionParams{
189
				TextDocumentPositionParams: protocol.TextDocumentPositionParams{
190
					TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},
191
					Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},
192
				},
193
			}
194
195
			// warm up
196
			if _, err := srv.server.Completion(b.Context(), params); err != nil {
197
				b.Fatal(err)
198
			}
199
			b.ReportAllocs()
200
			b.ResetTimer()
201
			for b.Loop() {
202
				if _, err := srv.server.Completion(b.Context(), params); err != nil {
203
					b.Fatal(err)
204
				}
205
			}
206
			// guard: line-local lexing must stay far below the old whole-file
207
			// relex (~3ms on this file); the measured target is sub-ms
208
			if avg := b.Elapsed() / time.Duration(b.N); avg > 2*time.Millisecond {
209
				b.Fatalf("completion %v/op: whole-file relex regression", avg)
210
			}
211
		})
212
	}
213
}