all repos

clerk @ 122b384

missing tooling for ledger/hledger
2 files changed, 156 insertions(+), 76 deletions(-)
lsp: refactor semantic tests
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-17 14:46:05 +0300
Authored at: 2026-08-16 20:56:00 +0300
Change ID: mmpnpunwusrzyqykvsysnvmluuoqssyk
Parent: f68a2c6
A internal/lsp/testdata/semantic-range.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-15 test

      
        
        3
        +    expenses:food  $50

      
        
        4
        +

      
        
        5
        +2024-01-16 other

      
        
        6
        +  expenses:drinks  $20

      
        
        7
        +

      
        
        8
        +2024-01-17 done

      
        
        9
        +  a  $1 = $100

      
        
        10
        +  b

      
        
        11
        +

      
        
        12
        +-- ranges.txt --

      
        
        13
        +first-txn 0 1

      
        
        14
        +second-txn 3 4

      
        
        15
        +blank-only 2 2

      
        
        16
        +whole-doc 0 8

      
        
        17
        +inverted 4 2

      
        
        18
        +

      
        
        19
        +-- expect --

      
        
        20
        +== first-txn ==

      
        
        21
        +0:0+10 class

      
        
        22
        +0:11+4 property

      
        
        23
        +1:4+13 namespace

      
        
        24
        +1:19+1 type

      
        
        25
        +1:20+2 number

      
        
        26
        +== second-txn ==

      
        
        27
        +3:0+10 class

      
        
        28
        +3:11+5 property

      
        
        29
        +4:2+15 namespace

      
        
        30
        +4:19+1 type

      
        
        31
        +4:20+2 number

      
        
        32
        +== blank-only ==

      
        
        33
        +== whole-doc ==

      
        
        34
        +0:0+10 class

      
        
        35
        +0:11+4 property

      
        
        36
        +1:4+13 namespace

      
        
        37
        +1:19+1 type

      
        
        38
        +1:20+2 number

      
        
        39
        +3:0+10 class

      
        
        40
        +3:11+5 property

      
        
        41
        +4:2+15 namespace

      
        
        42
        +4:19+1 type

      
        
        43
        +4:20+2 number

      
        
        44
        +6:0+10 class

      
        
        45
        +6:11+4 property

      
        
        46
        +7:2+1 namespace

      
        
        47
        +7:5+1 type

      
        
        48
        +7:6+1 number

      
        
        49
        +7:8+1 operator

      
        
        50
        +7:10+1 type

      
        
        51
        +7:11+3 number

      
        
        52
        +8:2+1 namespace

      
        
        53
        +== inverted ==

      
M internal/lsp/textdocument_semantic_tokens_test.go
···
        59
        59
         	}

      
        60
        60
         }

      
        61
        61
         

      
        62
        
        -func TestServer_Semantic_EmptyDocument(t *testing.T) {

      
        63
        
        -	srv := NewServer("test")

      
        64
        
        -	srv.server.openDoc(uri.URI("file:///empty.journal"), "", 1, "journal")

      
        65
        
        -

      
        66
        
        -	result, err := srv.server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        67
        
        -		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///empty.journal")},

      
        68
        
        -	})

      
        69
        
        -	if err != nil {

      
        70
        
        -		t.Fatal(err)

      
        71
        
        -	}

      
        72
        
        -	if len(result.Data) != 0 {

      
        73
        
        -		t.Errorf("expected empty data for empty doc, got %d values", len(result.Data))

      
        74
        
        -	}

      
        75
        
        -}

      
        76
        
        -

      
        77
        
        -func TestServer_Semantic_DocumentNotFound(t *testing.T) {

      
        78
        
        -	result, err := NewServer("test").server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        79
        
        -		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///unknown.journal")},

      
        80
        
        -	})

      
        81
        
        -	if err != nil {

      
        82
        
        -		t.Fatal(err)

      
        83
        
        -	}

      
        84
        
        -	if result == nil {

      
        85
        
        -		t.Fatal("result is nil")

      
        86
        
        -	}

      
        87
        
        -	if result.Data != nil {

      
        88
        
        -		t.Errorf("expected nil Data for unknown doc, got %v", result.Data)

      
        89
        
        -	}

      
        90
        
        -}

      
        91
        
        -

      
        92
        
        -func TestServer_Semantic_Range(t *testing.T) {

      
        93
        
        -	content := `2024-01-15 test

      
        94
        
        -    expenses:food  $50

      
        95
        
        -

      
        96
        
        -2024-01-16 other

      
        97
        
        -  expenses:drinks  $20

      
        98
        
        -`

      
        99
        
        -

      
        100
        
        -	srv := NewServer("test")

      
        101
        
        -	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")

      
        102
        
        -

      
        103
        
        -	result, err := srv.server.SemanticTokensRange(t.Context(), &protocol.SemanticTokensRangeParams{

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

      
        105
        
        -		Range: protocol.Range{

      
        106
        
        -			Start: protocol.Position{Line: 0, Character: 0},

      
        107
        
        -			End:   protocol.Position{Line: 1, Character: 50},

      
        108
        
        -		},

      
        109
        
        -	})

      
        110
        
        -	if err != nil {

      
        111
        
        -		t.Fatal(err)

      
        112
        
        -	}

      
        113
        
        -	if result == nil || len(result.Data) == 0 {

      
        114
        
        -		t.Fatal("expected non-empty tokens for range")

      
        115
        
        -	}

      
        116
        
        -

      
        117
        
        -	// decode and assert every token is inside the requested line range

      
        118
        
        -	line, col := 0, 0

      
        119
        
        -	for i := 0; i+4 < len(result.Data); i += 5 {

      
        120
        
        -		dl, dc := result.Data[i], result.Data[i+1]

      
        121
        
        -		if dl > 0 {

      
        122
        
        -			col = 0

      
        123
        
        -		}

      
        124
        
        -		line += int(dl)

      
        125
        
        -		col += int(dc)

      
        126
        
        -		if line > 1 {

      
        127
        
        -			t.Fatalf("token at line %d outside requested range [0,1]", line)

      
        128
        
        -		}

      
        129
        
        -	}

      
        130
        
        -}

      
        131
        
        -

      
        132
        62
         func TestSemanticTokensEdits(t *testing.T) {

      
        133
        63
         	tests := map[string]struct{ old, new []uint32 }{

      
        134
        64
         		"identical":      {[]uint32{1, 2, 3}, []uint32{1, 2, 3}},

      ···
        156
        86
         	}

      
        157
        87
         }

      
        158
        88
         

      
        159
        
        -// applySemEdits applies LSP semantic token edits to a client-side copy of data.

      
        160
        
        -func applySemEdits(data []uint32, edits []protocol.SemanticTokensEdit) []uint32 {

      
        161
        
        -	out := slices.Clone(data)

      
        162
        
        -	for _, e := range edits {

      
        163
        
        -		out = append(append(out[:e.Start], e.Data...), out[e.Start+e.DeleteCount:]...)

      
        
        89
        +func TestServer_Semantic_EmptyDocument(t *testing.T) {

      
        
        90
        +	srv := NewServer("test")

      
        
        91
        +	srv.server.openDoc(uri.URI("file:///empty.journal"), "", 1, "journal")

      
        
        92
        +	result, err := srv.server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        
        93
        +		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///empty.journal")},

      
        
        94
        +	})

      
        
        95
        +	if err != nil {

      
        
        96
        +		t.Fatal(err)

      
        
        97
        +	}

      
        
        98
        +	if len(result.Data) != 0 {

      
        
        99
        +		t.Errorf("expected empty data for empty doc, got %d values", len(result.Data))

      
        
        100
        +	}

      
        
        101
        +}

      
        
        102
        +

      
        
        103
        +func TestServer_Semantic_DocumentNotFound(t *testing.T) {

      
        
        104
        +	result, err := NewServer("test").server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        
        105
        +		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///unknown.journal")},

      
        
        106
        +	})

      
        
        107
        +	if err != nil {

      
        
        108
        +		t.Fatal(err)

      
        164
        109
         	}

      
        165
        
        -	return out

      
        
        110
        +	if result == nil {

      
        
        111
        +		t.Fatal("result is nil")

      
        
        112
        +	}

      
        
        113
        +	if result.Data != nil {

      
        
        114
        +		t.Errorf("expected nil Data for unknown doc, got %v", result.Data)

      
        
        115
        +	}

      
        166
        116
         }

      
        167
        117
         

      
        168
        118
         func TestGolden_SemanticTokens(t *testing.T) {

      ···
        180
        130
         	}

      
        181
        131
         }

      
        182
        132
         

      
        
        133
        +func TestGolden_SemanticTokensRange(t *testing.T) {

      
        
        134
        +	ar := golden.Read(t, "semantic-range")

      
        
        135
        +	in := ar.Get("in.journal")

      
        
        136
        +

      
        
        137
        +	t.Run("no-overlap", func(t *testing.T) {

      
        
        138
        +		assertGoldenNoOverlap(t, "semantic-range", ar)

      
        
        139
        +	})

      
        
        140
        +

      
        
        141
        +	t.Run("golden", func(t *testing.T) {

      
        
        142
        +		u := uri.URI("file:///test.journal")

      
        
        143
        +		srv := NewServer("test")

      
        
        144
        +		srv.server.openDoc(u, string(in), 1, "journal")

      
        
        145
        +

      
        
        146
        +		var out strings.Builder

      
        
        147
        +		for line := range strings.SplitSeq(string(ar.Get("ranges.txt")), "\n") {

      
        
        148
        +			if line == "" {

      
        
        149
        +				continue

      
        
        150
        +			}

      
        
        151
        +			var name string

      
        
        152
        +			var start, end uint32

      
        
        153
        +			if _, err := fmt.Sscanf(line, "%s %d %d", &name, &start, &end); err != nil {

      
        
        154
        +				t.Fatalf("ranges.txt: %q: %v", line, err)

      
        
        155
        +			}

      
        
        156
        +

      
        
        157
        +			res, err := srv.server.SemanticTokensRange(t.Context(), &protocol.SemanticTokensRangeParams{

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

      
        
        159
        +				Range: protocol.Range{

      
        
        160
        +					Start: protocol.Position{Line: start},

      
        
        161
        +					End:   protocol.Position{Line: end},

      
        
        162
        +				},

      
        
        163
        +			})

      
        
        164
        +			if err != nil {

      
        
        165
        +				t.Fatal(err)

      
        
        166
        +			}

      
        
        167
        +			tokens := decodeSemTokens(res.Data)

      
        
        168
        +			for _, tok := range tokens {

      
        
        169
        +				if tok.line < start || tok.line > end {

      
        
        170
        +					t.Fatalf("%s: token %d:%d outside range lines [%d,%d]", name, tok.line, tok.col, start, end)

      
        
        171
        +				}

      
        
        172
        +			}

      
        
        173
        +			fmt.Fprintf(&out, "== %s ==\n", name)

      
        
        174
        +			out.WriteString(renderSemanticTokens(tokens))

      
        
        175
        +		}

      
        
        176
        +		golden.Assert(t, ar, out.String())

      
        
        177
        +	})

      
        
        178
        +}

      
        
        179
        +

      
        183
        180
         func TestGolden_SemanticTokensDelta(t *testing.T) {

      
        184
        181
         	for _, tt := range []string{"semantic-delta-edit", "semantic-delta-nochange", "semantic-delta-stale"} {

      
        185
        182
         		ar := golden.Read(t, tt)

      ···
        243
        240
         			if want := encodeSemTokens(tokSem(finalText)); !slices.Equal(client, want) {

      
        244
        241
         				t.Errorf("delta flow produced %d elems, want %d", len(client), len(want))

      
        245
        242
         			}

      
        
        243
        +

      
        246
        244
         			out.WriteString(renderSemanticTokens(tokSem(finalText)))

      
        247
        245
         			golden.Assert(t, ar, out.String())

      
        248
        246
         		})

      ···
        300
        298
         	return b.String()

      
        301
        299
         }

      
        302
        300
         

      
        
        301
        +func decodeSemTokens(data []uint32) []semanticToken {

      
        
        302
        +	var out []semanticToken

      
        
        303
        +	line, col := 0, 0

      
        
        304
        +	for i := 0; i+4 < len(data); i += 5 {

      
        
        305
        +		if data[i] > 0 {

      
        
        306
        +			line += int(data[i])

      
        
        307
        +			col = int(data[i+1])

      
        
        308
        +		} else {

      
        
        309
        +			col += int(data[i+1])

      
        
        310
        +		}

      
        
        311
        +		out = append(out, semanticToken{

      
        
        312
        +			line:      uint32(line),

      
        
        313
        +			col:       uint32(col),

      
        
        314
        +			length:    data[i+2],

      
        
        315
        +			tokenType: data[i+3],

      
        
        316
        +			modifiers: data[i+4],

      
        
        317
        +		})

      
        
        318
        +	}

      
        
        319
        +	return out

      
        
        320
        +}

      
        
        321
        +

      
        303
        322
         func tokSem(content []byte) []semanticToken {

      
        304
        323
         	c := string(content)

      
        305
        324
         	return tokenizeForSemantics(c, parseJournalStr(c))

      ···
        344
        363
         	for b.Loop() {

      
        345
        364
         		_ = semanticTokensEdits(old, new)

      
        346
        365
         	}

      
        
        366
        +}

      
        
        367
        +

      
        
        368
        +func applySemEdits(data []uint32, edits []protocol.SemanticTokensEdit) []uint32 {

      
        
        369
        +	out := slices.Clone(data)

      
        
        370
        +	for _, e := range edits {

      
        
        371
        +		out = append(append(out[:e.Start], e.Data...), out[e.Start+e.DeleteCount:]...)

      
        
        372
        +	}

      
        
        373
        +	return out

      
        347
        374
         }

      
        348
        375
         

      
        349
        376
         func openJournal(t testing.TB, path string) string {