Skip to content

Commit 84f86ec

Browse files
authored
feat(references/resolve): support go-to-references for TL-B types and support multi-declarations (#744)
Fixes #742 Fixes #743
1 parent 2b3357a commit 84f86ec

File tree

6 files changed

+261
-34
lines changed

6 files changed

+261
-34
lines changed

server/src/languages/tlb/cache.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright © 2025 TON Studio
3-
import type {Ty} from "@server/languages/tact/types/BaseTy"
4-
import {TlbNode} from "@server/languages/tlb/psi/TlbNode"
3+
import {NamedNode} from "@server/languages/tlb/psi/TlbNode"
54

65
export class Cache<TKey, TValue> {
76
private readonly data: Map<TKey, TValue>
@@ -31,19 +30,14 @@ export class Cache<TKey, TValue> {
3130
}
3231

3332
export class CacheManager {
34-
public readonly typeCache: Cache<number, Ty | null>
35-
public readonly resolveCache: Cache<number, TlbNode | null>
33+
public readonly resolveCache: Cache<number, NamedNode[]>
3634

3735
public constructor() {
38-
this.typeCache = new Cache()
3936
this.resolveCache = new Cache()
4037
}
4138

4239
public clear(): void {
43-
console.info(
44-
`Clearing caches (types: ${this.typeCache.size}, resolve: ${this.resolveCache.size})`,
45-
)
46-
this.typeCache.clear()
40+
console.info(`Clearing caches (resolve: ${this.resolveCache.size})`)
4741
this.resolveCache.clear()
4842
}
4943
}

server/src/languages/tlb/find-definitions/index.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,21 @@ export function provideTlbDefinition(
1111
): lsp.Location[] | lsp.LocationLink[] {
1212
if (node.type !== "identifier" && node.type !== "type_identifier") return []
1313

14-
const target = TlbReference.resolve(new NamedNode(node, file))
15-
if (!target) return []
14+
const targets = TlbReference.multiResolve(new NamedNode(node, file))
15+
if (targets.length === 0) return []
1616

17-
if (target instanceof NamedNode) {
17+
return targets.map(target => {
1818
const nameNode = target.nameNode()
1919
if (nameNode) {
20-
return [
21-
{
22-
uri: file.uri,
23-
range: asLspRange(nameNode.node),
24-
},
25-
]
20+
return {
21+
uri: file.uri,
22+
range: asLspRange(nameNode.node),
23+
}
2624
}
27-
}
2825

29-
return [
30-
{
26+
return {
3127
uri: file.uri,
3228
range: asLspRange(target.node),
33-
},
34-
]
29+
}
30+
})
3531
}

server/src/languages/tlb/psi/TlbReference.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,46 @@ export class TlbReference {
1212
private readonly element: TlbNode
1313
private readonly file: TlbFile
1414

15-
public static resolve(node: TlbNode | null): TlbNode | null {
15+
public static resolve(node: TlbNode | null): NamedNode | null {
1616
if (node === null) return null
1717
return new TlbReference(node, node.file).resolve()
1818
}
1919

20+
public static multiResolve(node: TlbNode | null): NamedNode[] {
21+
if (node === null) return []
22+
return new TlbReference(node, node.file).multiResolve()
23+
}
24+
2025
public constructor(element: TlbNode, file: TlbFile) {
2126
this.element = element
2227
this.file = file
2328
}
2429

25-
public resolve(): TlbNode | null {
30+
public resolve(): NamedNode | null {
31+
const elements = this.multiResolve()
32+
return elements[0] ?? null
33+
}
34+
35+
public multiResolve(): NamedNode[] {
2636
return TLB_CACHE.resolveCache.cached(this.element.node.id, () => this.resolveImpl())
2737
}
2838

29-
public resolveImpl(): TlbNode | null {
30-
const result: TlbNode[] = []
39+
public resolveImpl(): NamedNode[] {
40+
const result: NamedNode[] = []
3141
const state = new ResolveState()
3242
this.processResolveVariants(
3343
TlbReference.createResolveProcessor(result, this.element),
3444
state,
3545
)
36-
if (result.length === 0) return null
37-
return result[0]
46+
if (result.length === 0) return []
47+
return result
3848
}
3949

4050
private static createResolveProcessor(result: TlbNode[], element: TlbNode): ScopeProcessor {
4151
return new (class implements ScopeProcessor {
4252
public execute(node: TlbNode, state: ResolveState): boolean {
4353
if (node.node.equals(element.node)) {
44-
result.push(node)
45-
return false
54+
return true
4655
}
4756

4857
if (!(node instanceof NamedNode) || !(element instanceof NamedNode)) {
@@ -53,7 +62,7 @@ export class TlbReference {
5362

5463
if (node.name() === searchName) {
5564
result.push(node)
56-
return false
65+
return true
5766
}
5867

5968
return true
@@ -63,8 +72,14 @@ export class TlbReference {
6372

6473
public processResolveVariants(proc: ScopeProcessor, state: ResolveState): boolean {
6574
const parent = this.element.node.parent
66-
if (parent?.type === "combinator") return true
67-
if (parent && parentOfType(parent, "type_parameter") !== null) return true
75+
if (parent?.type === "combinator") {
76+
const declaration = parent.parent
77+
if (declaration?.type === "declaration") {
78+
return proc.execute(new DeclarationNode(declaration, this.file), state)
79+
}
80+
}
81+
82+
if (parentOfType(this.element.node, "type_parameter") !== null) return true
6883

6984
for (const decl of this.file.getDeclarations()) {
7085
if (!proc.execute(decl, state)) return false
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2025 TON Studio
3+
import type {Node as SyntaxNode} from "web-tree-sitter"
4+
import {RecursiveVisitor} from "@server/languages/tact/psi/visitor"
5+
import {NamedNode, TlbNode} from "./TlbNode"
6+
import {TlbReference} from "./TlbReference"
7+
import type {TlbFile} from "./TlbFile"
8+
9+
/**
10+
* Describes a scope that contains all possible uses of a certain symbol.
11+
*/
12+
export interface SearchScope {
13+
toString(): string
14+
}
15+
16+
/**
17+
* Describes the scope described by some AST node; the search for usages will be
18+
* performed only within this node.
19+
*
20+
* For example, the scope for a local variable will be the block in which it is defined.
21+
*/
22+
export class LocalSearchScope implements SearchScope {
23+
public constructor(public node: SyntaxNode) {}
24+
25+
public toString(): string {
26+
return `LocalSearchScope:\n${this.node.text}`
27+
}
28+
}
29+
30+
/**
31+
* Describes a scope consisting of one or more files.
32+
*
33+
* For example, the scope of a global function from the standard library is all project files.
34+
*/
35+
export class GlobalSearchScope implements SearchScope {
36+
public constructor(public files: TlbFile[]) {}
37+
38+
public toString(): string {
39+
return `GlobalSearchScope:\n${this.files.map(f => `- ${f.uri}`).join("\n")}`
40+
}
41+
}
42+
43+
export interface FindTlbReferenceOptions {
44+
/**
45+
* if true, the first element of the result contains the definition
46+
*/
47+
readonly includeDefinition?: boolean
48+
/**
49+
* if true, don't include `self` as usages (for rename)
50+
*/
51+
readonly includeSelf?: boolean
52+
/**
53+
* if true, only TlbReferences from the same files listed
54+
*/
55+
readonly sameFileOnly?: boolean
56+
/**
57+
* search stops after `limit` number of TlbReferences are found
58+
*/
59+
readonly limit?: number
60+
}
61+
62+
/**
63+
* Referent encapsulates the logic for finding all TlbReferences to a definition.
64+
*/
65+
export class TlbReferent {
66+
private readonly resolved: NamedNode | null = null
67+
private readonly file: TlbFile
68+
69+
public constructor(node: SyntaxNode, file: TlbFile) {
70+
this.file = file
71+
const element = new NamedNode(node, file)
72+
this.resolved = TlbReference.resolve(element)
73+
}
74+
75+
/**
76+
* Returns a list of nodes that reference the definition.
77+
*/
78+
public findReferences({
79+
includeDefinition = false,
80+
sameFileOnly = false,
81+
limit = Infinity,
82+
}: FindTlbReferenceOptions): TlbNode[] {
83+
const resolved = this.resolved
84+
if (!resolved) return []
85+
86+
const useScope = this.useScope()
87+
if (!useScope) return []
88+
89+
const result: TlbNode[] = []
90+
if (includeDefinition && (!sameFileOnly || resolved.file.uri === this.file.uri)) {
91+
const nameNode = resolved.nameNode()
92+
if (nameNode) {
93+
result.push(nameNode)
94+
}
95+
}
96+
97+
this.searchInScope(useScope, sameFileOnly, result, limit)
98+
return result
99+
}
100+
101+
private searchInScope(
102+
scope: SearchScope,
103+
sameFileOnly: boolean,
104+
result: TlbNode[],
105+
limit: number,
106+
): void {
107+
if (!this.resolved) return
108+
109+
if (scope instanceof LocalSearchScope) {
110+
this.traverseTree(this.resolved.file, scope.node, result, limit)
111+
}
112+
113+
if (scope instanceof GlobalSearchScope) {
114+
if (sameFileOnly) {
115+
this.traverseTree(this.file, this.file.rootNode, result, limit)
116+
return
117+
}
118+
119+
for (const file of scope.files) {
120+
this.traverseTree(file, file.rootNode, result, limit)
121+
if (result.length === limit) {
122+
break
123+
}
124+
}
125+
}
126+
}
127+
128+
private traverseTree(file: TlbFile, node: SyntaxNode, result: TlbNode[], limit: number): void {
129+
const resolved = this.resolved
130+
if (!resolved) return
131+
132+
// The algorithm for finding TlbReferences is simple:
133+
// we traverse the node that contains all the uses and resolve
134+
// each identifier with the same name as searched symbol.
135+
// If that identifier refers to the definition we are looking for,
136+
// we add it to the list.
137+
RecursiveVisitor.visit(node, (node): boolean | "stop" => {
138+
// fast path, skip non-identifiers
139+
if (node.type !== "identifier" && node.type !== "type_identifier") {
140+
return true
141+
}
142+
143+
// fast path, identifier name doesn't equal to definition name
144+
const nodeName = node.text
145+
if (nodeName !== resolved.name()) {
146+
return true
147+
}
148+
149+
const parent = node.parent
150+
if (parent === null) return true
151+
152+
if (parent.type === "combinator") {
153+
return true
154+
}
155+
156+
const targets = TlbReference.multiResolve(new NamedNode(node, file))
157+
if (targets.length === 0) return true
158+
159+
for (const res of targets) {
160+
const identifier = res.nameIdentifier()
161+
if (!identifier) continue
162+
163+
if (
164+
res.node.type === resolved.node.type &&
165+
res.file.uri === resolved.file.uri &&
166+
res.node.startPosition.row === resolved.node.startPosition.row &&
167+
(identifier.text === resolved.name() || identifier.text === "self")
168+
) {
169+
// found new TlbReference
170+
result.push(new TlbNode(node, file))
171+
if (result.length === limit) {
172+
return "stop" // end iteration}
173+
}
174+
}
175+
}
176+
return true
177+
})
178+
}
179+
180+
/**
181+
* Returns the effective node in which all possible usages are expected.
182+
* Outside this node, no usages are assumed to exist. For example, a variable
183+
* can be used only in an outer block statement where it is defined.
184+
*/
185+
public useScope(): SearchScope | null {
186+
if (!this.resolved) return null
187+
188+
return new GlobalSearchScope([this.file])
189+
}
190+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type {Node as SyntaxNode} from "web-tree-sitter"
2+
import * as lsp from "vscode-languageserver"
3+
import {asLspRange} from "@server/utils/position"
4+
import {TlbReferent} from "@server/languages/tlb/psi/TlbReferent"
5+
import {TlbFile} from "@server/languages/tlb/psi/TlbFile"
6+
7+
export function provideTlbReferences(
8+
referenceNode: SyntaxNode,
9+
file: TlbFile,
10+
): lsp.Location[] | null {
11+
if (referenceNode.type !== "identifier" && referenceNode.type !== "type_identifier") {
12+
return []
13+
}
14+
15+
const result = new TlbReferent(referenceNode, file).findReferences({
16+
includeDefinition: false,
17+
})
18+
if (result.length === 0) return null
19+
20+
return result.map(value => ({
21+
uri: value.file.uri,
22+
range: asLspRange(value.node),
23+
}))
24+
}

server/src/server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {provideTactTypeAtPosition} from "@server/languages/tact/custom/type-at-p
9696
import {provideTlbDocumentSymbols} from "@server/languages/tlb/symbols"
9797
import {provideTlbCompletion} from "@server/languages/tlb/completion"
9898
import {TLB_CACHE} from "@server/languages/tlb/cache"
99+
import {provideTlbReferences} from "@server/languages/tlb/references"
99100

100101
/**
101102
* Whenever LS is initialized.
@@ -673,6 +674,13 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise<lsp.In
673674
return provideFiftReferences(node, file)
674675
}
675676

677+
if (isTlbFile(uri)) {
678+
const file = findTlbFile(uri)
679+
const node = nodeAtPosition(params, file)
680+
if (!node) return null
681+
return provideTlbReferences(node, file)
682+
}
683+
676684
if (isTactFile(uri)) {
677685
const file = findTactFile(uri)
678686
const node = nodeAtPosition(params, file)

0 commit comments

Comments
 (0)