Skip to content

Commit b935fa5

Browse files
committed
feat(acton): initial Acton integration
Fixes acton#137 Fixes acton#140 Fixes acton#215
1 parent 1d2ec17 commit b935fa5

6 files changed

Lines changed: 402 additions & 0 deletions

File tree

editors/code/src/acton/Acton.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2026 TON Core
3+
4+
import * as path from "node:path"
5+
6+
import vscode from "vscode"
7+
8+
import {ActonCommand} from "./ActonCommand"
9+
10+
export class Acton {
11+
private static instance: Acton | undefined
12+
13+
public static getInstance(): Acton {
14+
Acton.instance ??= new Acton()
15+
return Acton.instance
16+
}
17+
18+
public async execute(command: ActonCommand, workingDirectory?: string): Promise<void> {
19+
const actonPath = this.getActonPath()
20+
const args = command.getArguments()
21+
22+
const taskDefinition: vscode.TaskDefinition = {
23+
type: "acton",
24+
command: command.name,
25+
args: args,
26+
}
27+
28+
const workspaceFolder = workingDirectory
29+
? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(workingDirectory))
30+
: vscode.workspace.workspaceFolders?.[0]
31+
32+
const task = new vscode.Task(
33+
taskDefinition,
34+
workspaceFolder ?? vscode.TaskScope.Workspace,
35+
"acton",
36+
"acton",
37+
new vscode.ProcessExecution(actonPath, [command.name, ...args], {
38+
cwd: workingDirectory,
39+
}),
40+
)
41+
42+
task.presentationOptions = {
43+
reveal: vscode.TaskRevealKind.Always,
44+
panel: vscode.TaskPanelKind.Dedicated,
45+
clear: true,
46+
showReuseMessage: false,
47+
}
48+
49+
await vscode.tasks.executeTask(task)
50+
}
51+
52+
private getActonPath(): string {
53+
const config = vscode.workspace.getConfiguration("ton")
54+
const path = config.get<string>("acton.path")
55+
if (path && path.trim() !== "") {
56+
return path
57+
}
58+
return "acton"
59+
}
60+
61+
/**
62+
* Finds the nearest Acton.toml starting from the given file/directory and going up.
63+
*/
64+
public async findActonToml(startUri: vscode.Uri): Promise<vscode.Uri | undefined> {
65+
let current = startUri
66+
const root = vscode.workspace.getWorkspaceFolder(startUri)?.uri
67+
68+
for (let i = 0; i < 100; i++) {
69+
const tomlUri = vscode.Uri.joinPath(current, "Acton.toml")
70+
try {
71+
await vscode.workspace.fs.stat(tomlUri)
72+
return tomlUri
73+
} catch {
74+
// Not found
75+
}
76+
77+
if (root && current.toString() === root.toString()) {
78+
break
79+
}
80+
81+
const parent = vscode.Uri.file(path.dirname(current.fsPath))
82+
if (parent.toString() === current.toString()) {
83+
break
84+
}
85+
current = parent
86+
}
87+
88+
return undefined
89+
}
90+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2026 TON Core
3+
4+
export abstract class ActonCommand {
5+
protected constructor(public readonly name: string) {}
6+
7+
public abstract getArguments(): string[]
8+
}
9+
10+
export class BuildCommand extends ActonCommand {
11+
public constructor(
12+
public contractId: string = "",
13+
public clearCache: boolean = false,
14+
public outDir: string = "",
15+
) {
16+
super("build")
17+
}
18+
19+
public override getArguments(): string[] {
20+
const args: string[] = []
21+
if (this.clearCache) args.push("--clear-cache")
22+
if (this.outDir.trim() !== "") {
23+
args.push("--out-dir", this.outDir)
24+
}
25+
if (this.contractId.trim() !== "") {
26+
args.push(this.contractId)
27+
}
28+
return args
29+
}
30+
}
31+
32+
export enum TestMode {
33+
FUNCTION = "FUNCTION",
34+
FILE = "FILE",
35+
DIRECTORY = "DIRECTORY",
36+
}
37+
38+
export class TestCommand extends ActonCommand {
39+
public constructor(
40+
public mode: TestMode = TestMode.DIRECTORY,
41+
public target: string = "",
42+
public functionName: string = "",
43+
public clearCache: boolean = false,
44+
) {
45+
super("test")
46+
}
47+
48+
public override getArguments(): string[] {
49+
const args: string[] = ["--reporter", "console"]
50+
if (this.clearCache) args.push("--clear-cache")
51+
52+
switch (this.mode) {
53+
case TestMode.FUNCTION: {
54+
if (this.functionName.trim() !== "") {
55+
args.push("--filter", this.functionName.replace(/`/g, ""))
56+
}
57+
if (this.target.trim() !== "") {
58+
args.push(this.target)
59+
}
60+
break
61+
}
62+
case TestMode.FILE:
63+
case TestMode.DIRECTORY: {
64+
if (this.target.trim() !== "") {
65+
args.push(this.target)
66+
}
67+
break
68+
}
69+
}
70+
return args
71+
}
72+
}
73+
74+
export class ScriptCommand extends ActonCommand {
75+
public constructor(
76+
public scriptPath: string = "",
77+
public clearCache: boolean = false,
78+
public forkNet: string = "",
79+
public forkBlockNumber: string = "",
80+
public apiKey: string = "",
81+
public broadcast: boolean = false,
82+
public broadcastNet: string = "",
83+
public explorer: string = "",
84+
public debug: boolean = false,
85+
public debugPort: string = "",
86+
) {
87+
super("script")
88+
}
89+
90+
public override getArguments(): string[] {
91+
const args: string[] = []
92+
if (this.clearCache) args.push("--clear-cache")
93+
if (this.forkNet.trim() !== "") {
94+
args.push("--fork-net", this.forkNet)
95+
}
96+
if (this.forkBlockNumber.trim() !== "") {
97+
args.push("--fork-block-number", this.forkBlockNumber)
98+
}
99+
if (this.apiKey.trim() !== "") {
100+
args.push("--api-key", this.apiKey)
101+
}
102+
if (this.broadcast) {
103+
args.push("--broadcast")
104+
if (this.broadcastNet.trim() !== "") {
105+
args.push("--net", this.broadcastNet)
106+
}
107+
if (this.explorer.trim() !== "") {
108+
args.push("--explorer", this.explorer)
109+
}
110+
}
111+
if (this.debug) {
112+
args.push("--debug")
113+
if (this.debugPort.trim() !== "") {
114+
args.push("--debug-port", this.debugPort)
115+
}
116+
}
117+
if (this.scriptPath.trim() !== "") {
118+
args.push(this.scriptPath)
119+
}
120+
return args
121+
}
122+
}
123+
124+
export class RunCommand extends ActonCommand {
125+
public constructor(public scriptName: string = "") {
126+
super("run")
127+
}
128+
129+
public override getArguments(): string[] {
130+
const args: string[] = []
131+
if (this.scriptName.trim() !== "") {
132+
args.push(this.scriptName)
133+
}
134+
return args
135+
}
136+
}
137+
138+
export class CustomCommand extends ActonCommand {
139+
public constructor(
140+
command: string,
141+
public args: string[] = [],
142+
) {
143+
super(command)
144+
}
145+
146+
public override getArguments(): string[] {
147+
return this.args
148+
}
149+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2026 TON Core
3+
4+
import * as path from "node:path"
5+
6+
import * as vscode from "vscode"
7+
8+
import {Acton} from "../Acton"
9+
import {BuildCommand, RunCommand, TestCommand} from "../ActonCommand"
10+
11+
export class ActonTomlCodeLensProvider implements vscode.CodeLensProvider {
12+
private readonly _onDidChangeCodeLenses: vscode.EventEmitter<void> =
13+
new vscode.EventEmitter<void>()
14+
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event
15+
16+
public refresh(): void {
17+
this._onDidChangeCodeLenses.fire()
18+
}
19+
20+
public provideCodeLenses(
21+
document: vscode.TextDocument,
22+
_token: vscode.CancellationToken,
23+
): vscode.CodeLens[] {
24+
if (!document.fileName.endsWith("Acton.toml")) {
25+
return []
26+
}
27+
28+
const codeLenses: vscode.CodeLens[] = []
29+
const text = document.getText()
30+
const lines = text.split(/\r?\n/)
31+
32+
let currentSection: string | null = null
33+
34+
for (const [i, raw_line] of lines.entries()) {
35+
const line = raw_line.trim()
36+
if (line.startsWith("[") && line.endsWith("]")) {
37+
currentSection = line.slice(1, -1).trim()
38+
39+
if (currentSection === "test") {
40+
const range = new vscode.Range(i, 0, i, line.length)
41+
codeLenses.push(
42+
new vscode.CodeLens(range, {
43+
title: "Run all tests",
44+
command: "ton.acton.testAll",
45+
arguments: [document.uri.fsPath],
46+
}),
47+
)
48+
}
49+
50+
const contractMatch = /^contracts\.(.+)$/.exec(currentSection)
51+
if (contractMatch) {
52+
const contractId = contractMatch[1].trim()
53+
const range = new vscode.Range(i, 0, i, line.length)
54+
codeLenses.push(
55+
new vscode.CodeLens(range, {
56+
title: "Build contract",
57+
command: "ton.acton.buildContract",
58+
arguments: [document.uri.fsPath, contractId],
59+
}),
60+
)
61+
}
62+
}
63+
}
64+
65+
return codeLenses
66+
}
67+
68+
public static registerCommands(context: vscode.ExtensionContext): void {
69+
context.subscriptions.push(
70+
vscode.commands.registerCommand("ton.acton.testAll", async (tomlPath: string) => {
71+
const workingDir = path.dirname(tomlPath)
72+
await Acton.getInstance().execute(new TestCommand(), workingDir)
73+
}),
74+
vscode.commands.registerCommand(
75+
"ton.acton.buildContract",
76+
async (tomlPath: string, contractId: string) => {
77+
const workingDir = path.dirname(tomlPath)
78+
await Acton.getInstance().execute(new BuildCommand(contractId), workingDir)
79+
},
80+
),
81+
vscode.commands.registerCommand(
82+
"ton.acton.runScript",
83+
async (tomlPath: string, scriptName: string) => {
84+
const workingDir = path.dirname(tomlPath)
85+
await Acton.getInstance().execute(new RunCommand(scriptName), workingDir)
86+
},
87+
),
88+
)
89+
}
90+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2026 TON Core
3+
4+
import * as vscode from "vscode"
5+
6+
export class ActonTomlHoverProvider implements vscode.HoverProvider {
7+
public provideHover(
8+
document: vscode.TextDocument,
9+
position: vscode.Position,
10+
_token: vscode.CancellationToken,
11+
): vscode.Hover | null {
12+
if (!document.fileName.endsWith("Acton.toml")) {
13+
return null
14+
}
15+
16+
const line = document.lineAt(position.line).text
17+
const text = document.getText()
18+
const lines = text.split(/\r?\n/)
19+
20+
let currentSection: string | null = null
21+
for (let i = 0; i <= position.line; i++) {
22+
const l = lines[i].trim()
23+
if (l.startsWith("[") && l.endsWith("]")) {
24+
currentSection = l.slice(1, -1).trim()
25+
}
26+
}
27+
28+
if (currentSection !== "scripts") {
29+
return null
30+
}
31+
32+
const scriptMatch = /^([^\s#=]+)\s*=/.exec(line)
33+
if (scriptMatch) {
34+
const scriptName = scriptMatch[1]
35+
const scriptNameStart = line.indexOf(scriptName)
36+
const scriptNameEnd = scriptNameStart + scriptName.length
37+
38+
if (position.character >= scriptNameStart && position.character <= scriptNameEnd) {
39+
const args = [document.uri.fsPath, scriptName]
40+
const commandUri = vscode.Uri.parse(
41+
`command:ton.acton.runScript?${encodeURIComponent(JSON.stringify(args))}`,
42+
)
43+
const contents = new vscode.MarkdownString(`[Run Script](${commandUri.toString()})`)
44+
contents.isTrusted = true
45+
return new vscode.Hover(contents)
46+
}
47+
}
48+
49+
return null
50+
}
51+
}

0 commit comments

Comments
 (0)