Skip to content

Commit 61fc00c

Browse files
authoredFeb 21, 2025
feat(highlighting): highlight code in doc comments (#309)
Fixes #274
1 parent 2927877 commit 61fc00c

File tree

10 files changed

+895
-44
lines changed

10 files changed

+895
-44
lines changed
 

‎package.json

+6
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@
182182
"default": null,
183183
"description": "Path to Tact standard library. If empty, will try to find in node_modules"
184184
},
185+
"tact.highlighting.highlightCodeInComments": {
186+
"type": "boolean",
187+
"default": true,
188+
"description": "Highlight code in documentation comments"
189+
},
185190
"tact.toolchain.compilerPath": {
186191
"type": "string",
187192
"default": "",
@@ -383,6 +388,7 @@
383388
},
384389
"dependencies": {
385390
"@tact-lang/opcode": "^0.2.0",
391+
"@textlint/markdown-to-ast": "^14.4.2",
386392
"glob": "^11.0.1",
387393
"vscode-languageclient": "^8.0.2",
388394
"vscode-languageserver": "^8.0.2",

‎server/src/documentation/documentation.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {trimPrefix} from "@server/utils/strings"
1616
import * as compiler from "@server/compiler/utils"
1717
import {getDocumentSettings, TactSettings} from "@server/utils/settings"
1818
import {File} from "@server/psi/File"
19+
import {Position} from "vscode-languageclient"
20+
import {asLspPosition} from "@server/utils/position"
1921

2022
const CODE_FENCE = "```"
2123
const DOC_TMPL = `${CODE_FENCE}tact\n{signature}\n${CODE_FENCE}\n{documentation}\n`
@@ -306,14 +308,18 @@ function generateMemberDocFor(node: Node): string | null {
306308
return null
307309
}
308310

309-
export function extractCommentsDoc(node: Node): string {
311+
export function extractCommentsDocContent(node: Node): {
312+
content: string
313+
startPosition: Position
314+
} | null {
310315
const prevSibling = node.node.previousSibling
311-
if (!prevSibling || prevSibling.type !== "comment") return ""
316+
if (!prevSibling || prevSibling.type !== "comment") return null
317+
318+
const nodeStartLine = node.node.startPosition.row
312319

313320
const comments: SyntaxNode[] = []
314321
let comment: SyntaxNode | null = prevSibling
315322
while (comment?.type === "comment") {
316-
const nodeStartLine = node.node.startPosition.row
317323
const commentStartLine = comment.startPosition.row
318324

319325
if (commentStartLine + 1 + comments.length != nodeStartLine) {
@@ -324,11 +330,25 @@ export function extractCommentsDoc(node: Node): string {
324330
comment = comment.previousSibling
325331
}
326332

333+
if (comments.length === 0) return null
334+
327335
const finalComments = comments.reverse()
328336

329-
const lines = finalComments.map(c =>
330-
trimPrefix(trimPrefix(trimPrefix(c.text, "///"), "//"), " ").trimEnd(),
331-
)
337+
const content = finalComments
338+
.map(c => trimPrefix(trimPrefix(trimPrefix(c.text, "///"), "//"), " ").trimEnd())
339+
.join("\n")
340+
341+
return {
342+
content,
343+
startPosition: asLspPosition(comments[0].startPosition),
344+
}
345+
}
346+
347+
export function extractCommentsDoc(node: Node): string {
348+
const content = extractCommentsDocContent(node)
349+
if (!content) return ""
350+
351+
const lines = content.content.split("\n")
332352

333353
let result = ""
334354
let insideCodeBlock = false

‎server/src/psi/utils.ts

+21
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ export function isNamedFunNode(node: SyntaxNode): boolean {
3131
)
3232
}
3333

34+
export function isDocCommentOwner(node: SyntaxNode): boolean {
35+
return (
36+
node.type === "primitive" ||
37+
node.type === "global_constant" ||
38+
node.type === "native_function" ||
39+
node.type === "asm_function" ||
40+
node.type === "global_function" ||
41+
node.type === "struct" ||
42+
node.type === "message" ||
43+
node.type === "contract" ||
44+
node.type === "trait" ||
45+
node.type === "trait" ||
46+
node.type === "init_function" ||
47+
node.type === "receive_function" ||
48+
node.type === "bounced_function" ||
49+
node.type === "external_function" ||
50+
node.type === "storage_variable" ||
51+
node.type === "storage_constant"
52+
)
53+
}
54+
3455
export function funNodesTypes(): string[] {
3556
return ["global_function", "asm_function", "native_function", "storage_function"]
3657
}

‎server/src/semantic_tokens/collect.ts

+43-31
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,86 @@
11
import {RecursiveVisitor} from "@server/psi/visitor"
22
import type {File} from "@server/psi/File"
33
import {Reference} from "@server/psi/Reference"
4-
import type {Node as SyntaxNode} from "web-tree-sitter"
5-
import {NamedNode} from "@server/psi/Node"
4+
import {NamedNode, Node} from "@server/psi/Node"
65
import * as lsp from "vscode-languageserver"
76
import type {SemanticTokens} from "vscode-languageserver"
8-
import {isNamedFunNode} from "@server/psi/utils"
9-
10-
export function collect(file: File): SemanticTokens {
11-
const builder = new lsp.SemanticTokensBuilder()
12-
13-
function pushToken(n: SyntaxNode, tokenType: lsp.SemanticTokenTypes): void {
14-
builder.push(
15-
n.startPosition.row,
16-
n.startPosition.column,
17-
n.endPosition.column - n.startPosition.column,
18-
Object.keys(lsp.SemanticTokenTypes).indexOf(tokenType),
19-
0,
20-
)
21-
}
7+
import {isDocCommentOwner, isNamedFunNode} from "@server/psi/utils"
8+
import {createTactParser} from "@server/parser"
9+
import {extractCommentsDocContent} from "@server/documentation/documentation"
10+
import {processDocComment} from "@server/semantic_tokens/comments"
11+
import {Tokens} from "@server/semantic_tokens/tokens"
12+
13+
export function collect(
14+
file: File,
15+
highlighting: {
16+
highlightCodeInComments: boolean
17+
},
18+
): SemanticTokens {
19+
const tokens = new Tokens()
20+
21+
const parser = createTactParser()
2222

2323
RecursiveVisitor.visit(file.rootNode, (n): boolean => {
2424
const type = n.type
2525

2626
// asm fun foo() {}
2727
// ^^^ this
2828
if (type === "asm" && n.parent?.type === "asm_function") {
29-
pushToken(n, lsp.SemanticTokenTypes.keyword)
29+
tokens.node(n, lsp.SemanticTokenTypes.keyword)
3030
return true
3131
}
3232

3333
if (type === "global_constant") {
3434
const name = n.childForFieldName("name")
3535
if (!name) return true
36-
pushToken(name, lsp.SemanticTokenTypes.property)
36+
tokens.node(name, lsp.SemanticTokenTypes.property)
3737
return true
3838
}
3939

4040
if (type === "storage_function") {
4141
const name = n.childForFieldName("name")
4242
if (!name) return true
43-
pushToken(name, lsp.SemanticTokenTypes.function)
43+
tokens.node(name, lsp.SemanticTokenTypes.function)
4444
return true
4545
}
4646

4747
if (type === "parameter") {
4848
const name = n.childForFieldName("name")
4949
if (!name) return true
50-
pushToken(name, lsp.SemanticTokenTypes.parameter)
50+
tokens.node(name, lsp.SemanticTokenTypes.parameter)
5151
return true
5252
}
5353

5454
if (type === "let_statement") {
5555
const name = n.childForFieldName("name")
5656
if (!name) return true
57-
pushToken(name, lsp.SemanticTokenTypes.variable)
57+
tokens.node(name, lsp.SemanticTokenTypes.variable)
5858
return true
5959
}
6060

6161
if (type === "field" || type === "storage_variable") {
6262
const name = n.childForFieldName("name")
6363
if (!name) return true
64-
pushToken(name, lsp.SemanticTokenTypes.property)
64+
tokens.node(name, lsp.SemanticTokenTypes.property)
6565
return true
6666
}
6767

6868
if (type === "constant" || type === "storage_constant") {
6969
const name = n.childForFieldName("name")
7070
if (!name) return true
71-
pushToken(name, lsp.SemanticTokenTypes.enumMember)
71+
tokens.node(name, lsp.SemanticTokenTypes.enumMember)
7272
return true
7373
}
7474

7575
// asm fun foo() { ONE }
7676
// ^^^ this
7777
if (type === "tvm_instruction") {
78-
pushToken(n, lsp.SemanticTokenTypes.macro)
78+
tokens.node(n, lsp.SemanticTokenTypes.macro)
7979
return true
8080
}
8181

8282
if (type === "asm_stack_register") {
83-
pushToken(n, lsp.SemanticTokenTypes.parameter)
83+
tokens.node(n, lsp.SemanticTokenTypes.parameter)
8484
return true
8585
}
8686

@@ -92,31 +92,43 @@ export function collect(file: File): SemanticTokens {
9292

9393
switch (resolvedType) {
9494
case "parameter": {
95-
pushToken(n, lsp.SemanticTokenTypes.parameter)
95+
tokens.node(n, lsp.SemanticTokenTypes.parameter)
9696
break
9797
}
9898
case "field":
9999
case "storage_variable": {
100-
pushToken(n, lsp.SemanticTokenTypes.property)
100+
tokens.node(n, lsp.SemanticTokenTypes.property)
101101
break
102102
}
103103
case "constant":
104104
case "storage_constant": {
105-
pushToken(n, lsp.SemanticTokenTypes.enumMember)
105+
tokens.node(n, lsp.SemanticTokenTypes.enumMember)
106106
break
107107
}
108108
default: {
109109
if (isNamedFunNode(resolved.node)) {
110-
pushToken(n, lsp.SemanticTokenTypes.function)
110+
tokens.node(n, lsp.SemanticTokenTypes.function)
111111
} else if (resolved.node.parent?.type === "let_statement") {
112-
pushToken(n, lsp.SemanticTokenTypes.variable)
112+
tokens.node(n, lsp.SemanticTokenTypes.variable)
113113
}
114114
}
115115
}
116116
}
117117

118+
if (highlighting.highlightCodeInComments && isDocCommentOwner(n)) {
119+
const node = new Node(n, file)
120+
121+
const comment = extractCommentsDocContent(node)
122+
if (!comment) return true
123+
124+
processDocComment(tokens, comment, parser)
125+
}
126+
118127
return true
119128
})
120129

121-
return builder.build()
130+
return {
131+
resultId: Date.now().toString(),
132+
data: tokens.result(),
133+
}
122134
}

0 commit comments

Comments
 (0)