all repos

clerk @ 25c8f80b71674079b26e4189b4030a90fffb8c26

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: comply with lsp3.18 lifecycle, 29 days ago
1
package lsp
2
3
import (
4
	"fmt"
5
	"strings"
6
	"testing"
7
	"time"
8
9
	"go.lsp.dev/protocol"
10
	"go.lsp.dev/uri"
11
12
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
13
	"olexsmir.xyz/clerk/internal/testutil/golden"
14
	"olexsmir.xyz/clerk/journal/ast"
15
	"olexsmir.xyz/clerk/journal/token"
16
)
17
18
func TestTagValueSpan(t *testing.T) {
19
	tag := func(start, end int) *ast.Tag {
20
		return &ast.Tag{
21
			Span: token.Span{Start: token.Pos{Offset: start}, End: token.Pos{Offset: end}},
22
		}
23
	}
24
25
	tests := map[string]struct {
26
		content            string
27
		tag                *ast.Tag
28
		wantStart, wantEnd int
29
	}{
30
		"value":                {"; project:home", tag(2, 14), 10, 14},
31
		"empty value":          {"; done:", tag(2, 7), 7, 7},
32
		"trailing space":       {"; done: ", tag(2, 8), 7, 7},
33
		"second of comma list": {"; a:1, b:2", tag(7, 10), 9, 10},
34
	}
35
	for tname, tt := range tests {
36
		t.Run(tname, func(t *testing.T) {
37
			span := tagValueSpan(tt.content, tt.tag)
38
			if span.Start.Offset != tt.wantStart || span.End.Offset != tt.wantEnd {
39
				t.Errorf("got %d-%d, want %d-%d", span.Start.Offset, span.End.Offset, tt.wantStart, tt.wantEnd)
40
			}
41
		})
42
	}
43
}
44
45
func TestServer_Hover_DocumentNotFound(t *testing.T) {
46
	srv := newServer(t)
47
	res, err := srv.server.Hover(t.Context(), &protocol.HoverParams{
48
		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///nonexistent.journal")},
49
		Position:     protocol.Position{Line: 0, Character: 0},
50
	})
51
	if err != nil {
52
		t.Fatal(err)
53
	}
54
	if res != nil {
55
		t.Errorf("got %v, want nil", res)
56
	}
57
}
58
59
// Golden
60
61
func TestGolden_Hover(t *testing.T) {
62
	for _, tt := range []string{"hover-journal", "hover-include"} {
63
		t.Run(tt, func(t *testing.T) {
64
			ar := golden.Read(t, tt)
65
			h := newTxtarHarness(t, ar)
66
67
			var b strings.Builder
68
			for i := range h.cursors {
69
				tdp := h.textDocumentPosition(i)
70
				res, err := h.srv.Hover(t.Context(), &protocol.HoverParams{
71
					TextDocumentPositionParams: tdp,
72
				})
73
				if err != nil {
74
					t.Fatal(err)
75
				}
76
				if res == nil {
77
					fmt.Fprintf(&b, "%d:%d <none>\n", tdp.Position.Line, tdp.Position.Character)
78
					continue
79
				}
80
				mc, ok := res.Contents.(*protocol.MarkupContent)
81
				if !ok {
82
					t.Fatalf("Hover returned %T, want *MarkupContent", res.Contents)
83
				}
84
				fmt.Fprintf(&b, "%d:%d %q %d:%d-%d:%d\n",
85
					tdp.Position.Line, tdp.Position.Character, mc.Value,
86
					res.Range.Start.Line, res.Range.Start.Character, res.Range.End.Line, res.Range.End.Character)
87
			}
88
			golden.Assert(t, ar, b.String())
89
		})
90
	}
91
}
92
93
func BenchmarkHover(b *testing.B) {
94
	content := openJournal(b, "../../journal/testdata/journals/actual-1ktxns-100accts.journal")
95
96
	srv := newServer(b)
97
	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")
98
	srv.server.analysisFor(uri.URI("file:///test.journal")) // warm the per-doc cache
99
100
	for tname, tt := range map[string]int{
101
		"date":              strings.Index(content, "2000-01-01 transaction 1") + 3,
102
		"account":           strings.Index(content, "\n  1:2:3 ") + len("\n  ") + 2,
103
		"payee":             strings.Index(content, "transaction 1") + len("transaction ") + 1,
104
		"amount with cost":  strings.Index(content, "2 B @@") + 2,
105
		"commodity in cost": strings.Index(content, "@@ 2 C") + len("@@ 2 C") - 1,
106
		"late amount":       strings.Index(content, "1000 L @") + 2,
107
	} {
108
		b.Run(tname, func(b *testing.B) {
109
			line, col := lsputil.LineCol(content, tt)
110
			params := &protocol.HoverParams{
111
				TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},
112
				Position:     protocol.Position{Line: uint32(line), Character: uint32(col)},
113
			}
114
115
			// warm up: assert the cursor resolved to a hover
116
			res, err := srv.server.Hover(b.Context(), params)
117
			if err != nil {
118
				b.Fatal(err)
119
			}
120
			if res == nil {
121
				b.Fatalf("%s: no hover", tname)
122
			}
123
			b.ReportAllocs()
124
			b.ResetTimer()
125
			for b.Loop() {
126
				if _, err := srv.server.Hover(b.Context(), params); err != nil {
127
					b.Fatal(err)
128
				}
129
			}
130
131
			// guard: hover walks entries from the top of the file, so a cursor
132
			// near the end costs ~0.8ms on this file; a whole-file re-parse per
133
			// request (~13ms) would blow past this and must be caught
134
			if avg := b.Elapsed() / time.Duration(b.N); avg > 5*time.Millisecond {
135
				b.Fatalf("hover %v/op: entry walk or reparse regression", avg)
136
			}
137
		})
138
	}
139
}