Skip to content

Commit aa7b553

Browse files
authored
feat(tlb): add hover documentation for declarations and builtin types (#754)
Fixes #752 Fixes #753
1 parent 7f9db76 commit aa7b553

File tree

5 files changed

+184
-35
lines changed

5 files changed

+184
-35
lines changed

server/src/languages/tlb/completion/providers/BuiltinTypesCompletionProvider.ts

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,48 @@ import {
99
} from "@server/languages/tlb/completion/WeightedCompletionItem"
1010
import {CompletionItemKind} from "vscode-languageserver-types"
1111

12-
export class BuiltinTypesCompletionProvider implements CompletionProvider {
13-
private readonly types: [string, string][] = [
14-
["#", "Nat, 32-bit unsigned integer"],
15-
["##", "Nat: unsigned integer with `x` bits."],
16-
[
17-
"#<",
18-
"Nat: unsigned integer less than `x` bits, stored as `lenBits(x - 1)` bits up to 31 bits.",
19-
],
20-
[
21-
"#<=",
22-
"Nat: unsigned integer less than or equal to x bits, stored as lenBits(x) bits up to 32 bits.",
23-
],
24-
["Any", "remaining bits and references."],
25-
["Cell", "remaining bits and references."],
26-
["Int", "257 bits"],
27-
["UInt", "256 bits"],
28-
["Bits", "1023 bits"],
29-
["bits", "X bits"],
30-
["uint", ""],
31-
["uint8", ""],
32-
["uint16", ""],
33-
["uint32", ""],
34-
["uint64", ""],
35-
["uint128", ""],
36-
["uint256", ""],
37-
["int", ""],
38-
["int8", ""],
39-
["int16", ""],
40-
["int32", ""],
41-
["int64", ""],
42-
["int128", ""],
43-
["int256", ""],
44-
["int257", ""],
45-
]
12+
export const BUILTIN_TYPES: Map<string, string> = new Map([
13+
["#", "Nat, 32-bit unsigned integer"],
14+
["##", "Nat: unsigned integer with `x` bits"],
15+
[
16+
"#<",
17+
"Nat: unsigned integer less than `x` stored with the minimum number `⌈log2 x⌉` of bits (up to 31) to represent the number `x`",
18+
],
19+
[
20+
"#<=",
21+
"Nat: unsigned integer less than or equal `x` stored with the minimum number `⌈log2(x+1)⌉` of bits (up to 32) to represent the number `x`",
22+
],
23+
["Any", "Remaining bits and references"],
24+
["Cell", "Remaining bits and references"],
25+
["Int", "257 bits"],
26+
["UInt", "256 bits"],
27+
["Bits", "1023 bits"],
28+
["bits", "X bits"],
29+
["uint", ""],
30+
["uint8", ""],
31+
["uint16", ""],
32+
["uint32", ""],
33+
["uint64", ""],
34+
["uint128", ""],
35+
["uint256", ""],
36+
["int", ""],
37+
["int8", ""],
38+
["int16", ""],
39+
["int32", ""],
40+
["int64", ""],
41+
["int128", ""],
42+
["int256", ""],
43+
["int257", ""],
44+
["Type", "Built-in TL-B type representing the type of types"],
45+
])
4646

47+
export class BuiltinTypesCompletionProvider implements CompletionProvider {
4748
public isAvailable(ctx: CompletionContext): boolean {
4849
return ctx.isType
4950
}
5051

5152
public addCompletion(_ctx: CompletionContext, result: CompletionResult): void {
52-
for (const [type, description] of this.types) {
53+
for (const [type, description] of BUILTIN_TYPES) {
5354
result.add({
5455
label: type,
5556
labelDetails: {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2025 TON Studio
3+
import {NamedNode} from "@server/languages/tlb/psi/TlbNode"
4+
5+
const CODE_FENCE = "```"
6+
7+
export function generateDeclarationDoc(decls: NamedNode[]): string {
8+
let result = `${CODE_FENCE}\n`
9+
10+
decls.forEach((decl, index) => {
11+
result += `${decl.node.text}\n`
12+
13+
if (index !== decls.length - 1) {
14+
result += `\n`
15+
}
16+
})
17+
18+
result += `\n${CODE_FENCE}`
19+
return result
20+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as lsp from "vscode-languageserver"
2+
import type {Node as SyntaxNode} from "web-tree-sitter"
3+
import {TlbFile} from "@server/languages/tlb/psi/TlbFile"
4+
import {asLspRange} from "@server/utils/position"
5+
import {DeclarationNode, NamedNode} from "@server/languages/tlb/psi/TlbNode"
6+
import {TlbReference} from "@server/languages/tlb/psi/TlbReference"
7+
import {BUILTIN_TYPES} from "@server/languages/tlb/completion/providers/BuiltinTypesCompletionProvider"
8+
import {generateDeclarationDoc} from "@server/languages/tlb/documentation/documentation"
9+
10+
export function provideTlbDocumentation(hoverNode: SyntaxNode, file: TlbFile): lsp.Hover | null {
11+
function generateKeywordDoc(node: SyntaxNode, doc: string): lsp.Hover | null {
12+
return {
13+
range: asLspRange(node),
14+
contents: {
15+
kind: "markdown",
16+
value: doc,
17+
},
18+
}
19+
}
20+
21+
const text = hoverNode.text
22+
23+
const builtinDoc = BUILTIN_TYPES.get(text)
24+
if (builtinDoc) {
25+
const parent = hoverNode.parent
26+
if (parent?.type === "constructor_tag") return null
27+
return generateKeywordDoc(hoverNode, builtinDoc)
28+
}
29+
30+
if (hoverNode.type !== "identifier" && hoverNode.type !== "type_identifier") {
31+
return null
32+
}
33+
34+
const results = TlbReference.multiResolve(new NamedNode(hoverNode, file))
35+
if (results.length === 0) {
36+
const typeDoc = generateTypeDoc(text)
37+
if (typeDoc) {
38+
return {
39+
range: asLspRange(hoverNode),
40+
contents: {
41+
kind: "markdown",
42+
value: typeDoc,
43+
},
44+
}
45+
}
46+
return null
47+
}
48+
49+
if (results[0] instanceof DeclarationNode) {
50+
const declDoc = generateDeclarationDoc(results)
51+
return {
52+
range: asLspRange(hoverNode),
53+
contents: {
54+
kind: "markdown",
55+
value: declDoc,
56+
},
57+
}
58+
}
59+
60+
return null
61+
}
62+
63+
interface TypeDoc {
64+
readonly label: string
65+
readonly range: string
66+
readonly size: string
67+
readonly description?: string
68+
}
69+
70+
function generateTypeDoc(word: string): string | undefined {
71+
const typeInfo = generateArbitraryIntDoc(word)
72+
if (!typeInfo) return undefined
73+
74+
return `
75+
**${word}** — ${typeInfo.label}
76+
77+
- **Range**: ${typeInfo.range}
78+
- **Size**: ${typeInfo.size}
79+
`
80+
}
81+
82+
function generateArbitraryIntDoc(type: string): TypeDoc | undefined {
83+
const match = /^(u?int|bits)(\d+)$/.exec(type)
84+
if (!match) return undefined
85+
86+
const [_, prefix, bits] = match
87+
const bitWidth = Number.parseInt(bits)
88+
89+
if (prefix === "uint" && (bitWidth < 1 || bitWidth > 256)) return undefined
90+
if (prefix === "int" && (bitWidth < 1 || bitWidth > 257)) return undefined
91+
if (prefix === "bits" && (bitWidth < 1 || bitWidth > 257)) return undefined
92+
93+
if (prefix === "bits") {
94+
return {
95+
label: `${bitWidth}-bit data`,
96+
range: `0 to ${bitWidth} bits`,
97+
size: `${bitWidth} bits`,
98+
description: "Arbitrary bit-width data",
99+
}
100+
}
101+
102+
if (prefix === "uint") {
103+
return {
104+
label: `${bitWidth}-bit unsigned integer`,
105+
range: `0 to 2^${bitWidth} - 1`,
106+
size: `${bitWidth} bits`,
107+
description: "Arbitrary bit-width unsigned integer type",
108+
}
109+
}
110+
111+
return {
112+
label: `${bitWidth}-bit signed integer`,
113+
range: `-2^${bitWidth - 1} to 2^${bitWidth - 1} - 1`,
114+
size: `${bitWidth} bits`,
115+
description: "Arbitrary bit-width signed integer type",
116+
}
117+
}

server/src/languages/tlb/semantic-tokens/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export function provideTlbSemanticTokens(file: TlbFile): SemanticTokens {
2929
case "#<":
3030
case "#<=":
3131
case "builtin_field": {
32+
const parent = node.parent
33+
if (parent?.type === "constructor_tag") break
34+
3235
pushToken(node, SemanticTokenTypes.macro)
3336
break
3437
}

server/src/server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
} from "@server/files"
6262
import {provideTactDocumentation} from "@server/languages/tact/documentation"
6363
import {provideFiftDocumentation} from "@server/languages/fift/documentation"
64+
import {provideTlbDocumentation} from "@server/languages/tlb/documentation"
6465
import {
6566
provideTactDefinition,
6667
provideTactTypeDefinition,
@@ -518,6 +519,13 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise<lsp.In
518519
return provideFiftDocumentation(hoverNode, file)
519520
}
520521

522+
if (isTlbFile(uri)) {
523+
const file = findTlbFile(uri)
524+
const hoverNode = nodeAtPosition(params, file)
525+
if (!hoverNode) return null
526+
return provideTlbDocumentation(hoverNode, file)
527+
}
528+
521529
if (isTactFile(uri)) {
522530
const file = findTactFile(params.textDocument.uri)
523531
const hoverNode = nodeAtPosition(params, file)

0 commit comments

Comments
 (0)