Skip to content

Commit 1f5fa02

Browse files
authored
feat(acton): add very initial support for acton fmt (#280)
1 parent 91e9e8d commit 1f5fa02

4 files changed

Lines changed: 130 additions & 2 deletions

File tree

editors/code/src/acton/ActonCommand.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export class CheckCommand extends ActonCommand {
155155
public override getArguments(): string[] {
156156
const args: string[] = []
157157
if (this.json) {
158-
args.push("--json")
158+
args.push("--output-format=json")
159159
}
160160
if (this.target.trim() !== "") {
161161
args.push(this.target)
@@ -164,6 +164,16 @@ export class CheckCommand extends ActonCommand {
164164
}
165165
}
166166

167+
export class FormatCommand extends ActonCommand {
168+
public constructor(public targets: readonly string[] = []) {
169+
super("fmt")
170+
}
171+
172+
public override getArguments(): string[] {
173+
return this.targets.filter(target => target.trim() !== "")
174+
}
175+
}
176+
167177
export class CustomCommand extends ActonCommand {
168178
public constructor(
169179
command: string,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright © 2026 TON Core
3+
4+
import * as fs from "node:fs/promises"
5+
import * as os from "node:os"
6+
import * as path from "node:path"
7+
8+
import * as vscode from "vscode"
9+
10+
import {consoleError} from "../client-log"
11+
12+
import {Acton} from "./Acton"
13+
import {FormatCommand} from "./ActonCommand"
14+
15+
function isActonUnavailableError(error: unknown): error is NodeJS.ErrnoException {
16+
return error instanceof Error && "code" in error && error.code === "ENOENT"
17+
}
18+
19+
function fullDocumentRange(document: vscode.TextDocument): vscode.Range {
20+
const lastLine = document.lineCount - 1
21+
const end = document.lineAt(lastLine).range.end
22+
return new vscode.Range(new vscode.Position(0, 0), end)
23+
}
24+
25+
async function resolveWorkingDirectory(document: vscode.TextDocument): Promise<string | undefined> {
26+
if (document.uri.scheme === "file") {
27+
const actonToml = await Acton.getInstance().findActonToml(document.uri)
28+
if (actonToml) {
29+
return path.dirname(actonToml.fsPath)
30+
}
31+
32+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
33+
if (workspaceFolder) {
34+
return workspaceFolder.uri.fsPath
35+
}
36+
}
37+
38+
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
39+
}
40+
41+
export async function formatTolkDocumentWithActon(
42+
document: vscode.TextDocument,
43+
): Promise<vscode.TextEdit[] | null> {
44+
const formatterEnabled = vscode.workspace
45+
.getConfiguration("ton", document.uri)
46+
.get<boolean>("tolk.formatter.useFormatter", true)
47+
if (!formatterEnabled) {
48+
return []
49+
}
50+
51+
const source = document.getText()
52+
const workingDirectory = await resolveWorkingDirectory(document)
53+
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "acton-fmt-"))
54+
55+
try {
56+
const originalFileName =
57+
document.uri.scheme === "file" ? path.basename(document.uri.fsPath) : "untitled.tolk"
58+
const tempFilePath = path.join(tempDirectory, originalFileName)
59+
await fs.writeFile(tempFilePath, source, "utf8")
60+
61+
const command = new FormatCommand([tempFilePath])
62+
const {exitCode, stderr, stdout} = await Acton.getInstance().spawn(
63+
command,
64+
workingDirectory,
65+
)
66+
67+
if (exitCode !== 0) {
68+
const details = (stderr.trim() || stdout.trim() || `exit code ${exitCode}`).split(
69+
"\n",
70+
)[0]
71+
void vscode.window.showErrorMessage(`Failed to format with acton fmt: ${details}`)
72+
return []
73+
}
74+
75+
const formatted = await fs.readFile(tempFilePath, "utf8")
76+
if (formatted === source) {
77+
return []
78+
}
79+
80+
return [vscode.TextEdit.replace(fullDocumentRange(document), formatted)]
81+
} catch (error) {
82+
if (isActonUnavailableError(error)) {
83+
return null
84+
}
85+
86+
consoleError("Failed to format with acton fmt", error)
87+
void vscode.window.showErrorMessage("Failed to format with acton fmt")
88+
return []
89+
} finally {
90+
await fs.rm(tempDirectory, {recursive: true, force: true}).catch(() => {
91+
// ignore cleanup errors for temporary files
92+
})
93+
}
94+
}

editors/code/src/extension.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {ActonTomlHoverProvider} from "./acton/toml/ActonTomlHoverProvider"
5454
import {ActonTolkCodeLensProvider} from "./acton/tolk/ActonTolkCodeLensProvider"
5555
import {ActonLinter} from "./acton/ActonLinter"
5656
import {ActonTestController} from "./acton/ActonTestController"
57+
import {formatTolkDocumentWithActon} from "./acton/ActonFormatter"
5758
import {configureDebugging} from "./debugging"
5859
import {ContractData, TransactionRun} from "./providers/sandbox/test-types"
5960
import {TransactionDetailsInfo} from "./common/types/transaction"
@@ -296,6 +297,28 @@ async function startServer(context: vscode.ExtensionContext): Promise<vscode.Dis
296297
configurationSection: "ton",
297298
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.{tolk,fc,func,tlb}"),
298299
},
300+
middleware: {
301+
provideDocumentFormattingEdits: async (document, options, token, next) => {
302+
if (document.languageId !== "tolk") {
303+
return next(document, options, token)
304+
}
305+
306+
const actonEdits = await formatTolkDocumentWithActon(document)
307+
if (actonEdits !== null) {
308+
return actonEdits
309+
}
310+
311+
return next(document, options, token)
312+
},
313+
provideDocumentRangeFormattingEdits: async (document, range, options, token, next) => {
314+
if (document.languageId !== "tolk") {
315+
return next(document, range, options, token)
316+
}
317+
318+
// acton fmt formats whole files only
319+
return []
320+
},
321+
},
299322
initializationOptions: {
300323
clientConfig: getClientConfiguration(),
301324
treeSitterWasmUri: vscode_uri.joinPath(context.extensionUri, "./dist/tree-sitter.wasm")

server/src/languages/tolk/types/ty.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,8 @@ export function joinTypes(left: Ty, right: Ty): Ty {
929929
// example: `int?` - `null` = `int`
930930
// example: `int | slice | builder | bool` - `bool | slice` = `int | builder`
931931
// what for: `if (x != null)` / `if (x is T)`, to smart cast x inside if
932-
export function subtractTypes(left: Ty | null, right: Ty): Ty {
932+
export function subtractTypes(left_: Ty | null, right: Ty): Ty {
933+
const left = left_?.unwrapAlias()
933934
if (!left) return NeverTy.NEVER
934935
if (!(left instanceof UnionTy)) return left
935936

0 commit comments

Comments
 (0)