diff --git a/client/src/build-system.ts b/client/src/build-system.ts index 0880fcc4..7b2d013e 100644 --- a/client/src/build-system.ts +++ b/client/src/build-system.ts @@ -1,14 +1,11 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio import * as vscode from "vscode" -import * as fs from "node:fs" -import * as path from "node:path" -import {TaskDefinition} from "vscode" interface TaskProviderBase extends vscode.TaskProvider { createTask(): vscode.Task - isAvailable(): boolean + isAvailable(): Promise readonly taskType: string } @@ -26,13 +23,13 @@ export class BlueprintTaskProvider implements TaskProviderBase { this.group = group } - public provideTasks(): vscode.Task[] { - const isAvailable = this.isAvailable() + public async provideTasks(): Promise { + const isAvailable = await this.isAvailable() if (!isAvailable) return [] return [this.createTask()] } - public isAvailable(): boolean { + public async isAvailable(): Promise { return projectUsesBlueprint() } @@ -45,7 +42,7 @@ export class BlueprintTaskProvider implements TaskProviderBase { } public createTask(): vscode.Task { - const definition: TaskDefinition = { + const definition: vscode.TaskDefinition = { type: this.taskType, } @@ -88,12 +85,12 @@ export class TactTemplateTaskProvider implements TaskProviderBase { this.group = group } - public isAvailable(): boolean { - return !projectUsesBlueprint() + public async isAvailable(): Promise { + return !(await projectUsesBlueprint()) } - public provideTasks(): vscode.Task[] { - const isAvailable = this.isAvailable() + public async provideTasks(): Promise { + const isAvailable = await this.isAvailable() if (!isAvailable) return [] return [this.createTask()] } @@ -107,7 +104,7 @@ export class TactTemplateTaskProvider implements TaskProviderBase { } public createTask(): vscode.Task { - const definition: TaskDefinition = { + const definition: vscode.TaskDefinition = { type: this.taskType, } @@ -137,19 +134,22 @@ export class TactTemplateTaskProvider implements TaskProviderBase { } } -function registerTaskProvider(context: vscode.ExtensionContext, provider: TaskProviderBase): void { - if (!provider.isAvailable()) return +async function registerTaskProvider( + context: vscode.ExtensionContext, + provider: TaskProviderBase, +): Promise { + if (!(await provider.isAvailable())) return const taskProviderDisposable = vscode.tasks.registerTaskProvider(provider.taskType, provider) context.subscriptions.push(taskProviderDisposable) } -export function registerBuildTasks(context: vscode.ExtensionContext): void { - registerTaskProvider( +export async function registerBuildTasks(context: vscode.ExtensionContext): Promise { + await registerTaskProvider( context, new BlueprintTaskProvider("build", "build", "npx blueprint build", vscode.TaskGroup.Build), ) - registerTaskProvider( + await registerTaskProvider( context, new BlueprintTaskProvider( "build-all", @@ -158,11 +158,11 @@ export function registerBuildTasks(context: vscode.ExtensionContext): void { vscode.TaskGroup.Build, ), ) - registerTaskProvider( + await registerTaskProvider( context, new BlueprintTaskProvider("test", "test", "npx blueprint test", vscode.TaskGroup.Test), ) - registerTaskProvider( + await registerTaskProvider( context, new BlueprintTaskProvider( "build-and-test-all", @@ -171,15 +171,15 @@ export function registerBuildTasks(context: vscode.ExtensionContext): void { vscode.TaskGroup.Build, ), ) - registerTaskProvider( + await registerTaskProvider( context, new TactTemplateTaskProvider("build", "build", "yarn build", vscode.TaskGroup.Build), ) - registerTaskProvider( + await registerTaskProvider( context, new TactTemplateTaskProvider("test", "test", "yarn test", vscode.TaskGroup.Test), ) - registerTaskProvider( + await registerTaskProvider( context, new TactTemplateTaskProvider( "build-and-test", @@ -208,14 +208,15 @@ export function registerBuildTasks(context: vscode.ExtensionContext): void { ) } -function projectUsesBlueprint(): boolean { +async function projectUsesBlueprint(): Promise { const workspaceFolders = vscode.workspace.workspaceFolders if (!workspaceFolders || workspaceFolders.length === 0) return false - const packageJsonPath = path.join(workspaceFolders[0].uri.fsPath, "package.json") - try { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { + const packageJsonContent = await vscode.workspace.fs.readFile( + vscode.Uri.joinPath(workspaceFolders[0].uri, "package.json"), + ) + const packageJson = JSON.parse(new TextDecoder().decode(packageJsonContent)) as { dependencies?: Record devDependencies?: Record } diff --git a/client/src/commands/saveBocDecompiledCommand.ts b/client/src/commands/saveBocDecompiledCommand.ts index d98678d7..53cd9fb8 100644 --- a/client/src/commands/saveBocDecompiledCommand.ts +++ b/client/src/commands/saveBocDecompiledCommand.ts @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio import * as vscode from "vscode" -import * as path from "node:path" -import * as fs from "node:fs" import {BocDecompilerProvider} from "../providers/BocDecompilerProvider" -import {Disposable} from "vscode" -export function registerSaveBocDecompiledCommand(_context: vscode.ExtensionContext): Disposable { +export function registerSaveBocDecompiledCommand( + _context: vscode.ExtensionContext, +): vscode.Disposable { return vscode.commands.registerCommand( "tact.saveBocDecompiled", async (fileUri: vscode.Uri | undefined) => { @@ -46,16 +45,14 @@ async function saveBoc(fileUri: vscode.Uri | undefined): Promise { scheme: BocDecompilerProvider.scheme, path: actualFileUri.path + ".decompiled.fif", }) - const content = decompiler.provideTextDocumentContent(decompileUri) + const content = await decompiler.provideTextDocumentContent(decompileUri) const outputPath = actualFileUri.fsPath + ".decompiled.fif" - fs.writeFileSync(outputPath, content) + const bytes = new TextEncoder().encode(content) + vscode.workspace.fs.writeFile(vscode.Uri.file(outputPath), bytes) - const relativePath = path.relative( - vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "", - outputPath, - ) + const relativePath = vscode.workspace.asRelativePath(outputPath) vscode.window.showInformationMessage(`Decompiled BOC saved to: ${relativePath}`) const savedFileUri = vscode.Uri.file(outputPath) diff --git a/client/src/extension.ts b/client/src/extension.ts index 9e39bc7b..d1cb7a69 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio import * as vscode from "vscode" -import * as fs from "node:fs" import * as path from "node:path" import {Utils as vscode_uri} from "vscode-uri" import { @@ -44,9 +43,9 @@ let client: LanguageClient | null = null let gasStatusBarItem: vscode.StatusBarItem | null = null let cachedToolchainInfo: SetToolchainVersionParams | null = null -export function activate(context: vscode.ExtensionContext): void { +export async function activate(context: vscode.ExtensionContext): Promise { startServer(context).catch(consoleError) - registerBuildTasks(context) + await registerBuildTasks(context) registerOpenBocCommand(context) registerSaveBocDecompiledCommand(context) registerMistiCommand(context) @@ -817,14 +816,15 @@ function getInstallCommandForMisti(packageManager: PackageManager): string { } } -function projectUsesMisti(): boolean { +async function projectUsesMisti(): Promise { const workspaceFolders = vscode.workspace.workspaceFolders if (!workspaceFolders || workspaceFolders.length === 0) return false - const packageJsonPath = path.join(workspaceFolders[0].uri.fsPath, "package.json") - try { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { + const packageJsonContent = await vscode.workspace.fs.readFile( + vscode.Uri.joinPath(workspaceFolders[0].uri, "package.json"), + ) + const packageJson = JSON.parse(new TextDecoder().decode(packageJsonContent)) as { dependencies?: Record devDependencies?: Record } @@ -842,8 +842,8 @@ function projectUsesMisti(): boolean { function registerMistiCommand(context: vscode.ExtensionContext): void { context.subscriptions.push( vscode.commands.registerCommand("tact.runMisti", async () => { - if (!projectUsesMisti()) { - const packageManager = detectPackageManager() + if (!(await projectUsesMisti())) { + const packageManager = await detectPackageManager() const installCommand = getInstallCommandForMisti(packageManager) const result = await vscode.window.showErrorMessage( diff --git a/client/src/providers/BocDecompilerProvider.ts b/client/src/providers/BocDecompilerProvider.ts index 44306ff0..f063033b 100644 --- a/client/src/providers/BocDecompilerProvider.ts +++ b/client/src/providers/BocDecompilerProvider.ts @@ -2,41 +2,37 @@ // Copyright © 2025 TON Studio import * as vscode from "vscode" import {AssemblyWriter, Cell, debugSymbols, disassembleRoot} from "@tact-lang/opcode" -import {readFileSync, existsSync} from "node:fs" export class BocDecompilerProvider implements vscode.TextDocumentContentProvider { private readonly _onDidChange: vscode.EventEmitter = new vscode.EventEmitter() public readonly onDidChange: vscode.Event = this._onDidChange.event - private readonly lastModified: Map = new Map() + private readonly lastModified: Map = new Map() public static scheme: string = "boc-decompiled" - public provideTextDocumentContent(uri: vscode.Uri): string { - const bocPath = this.getBocPath(uri) + public async provideTextDocumentContent(uri: vscode.Uri): Promise { + const bocUri = this.getBocPath(uri) try { - return this.decompileBoc(bocPath) + return await this.decompileBoc(bocUri) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) return this.formatError(errorMessage) } } - private getBocPath(uri: vscode.Uri): string { + private getBocPath(uri: vscode.Uri): vscode.Uri { console.log("Original URI:", uri.toString()) const bocPath = uri.fsPath.replace(".decompiled.fif", "") console.log("BOC path:", bocPath) - return bocPath + return vscode.Uri.file(bocPath) } - private decompileBoc(bocPath: string): string { + private async decompileBoc(bocUri: vscode.Uri): Promise { try { - if (!existsSync(bocPath)) { - throw new Error(`BoC file not found: ${bocPath}`) - } - - const content = readFileSync(bocPath).toString("base64") + const rawContent = await vscode.workspace.fs.readFile(bocUri) + const content = Buffer.from(rawContent).toString("base64") const cell = Cell.fromBase64(content) const program = disassembleRoot(cell, { computeRefs: true, @@ -47,19 +43,19 @@ export class BocDecompilerProvider implements vscode.TextDocumentContentProvider debugSymbols: debugSymbols, }) - return this.formatDecompiledOutput(output, bocPath) + return this.formatDecompiledOutput(output, bocUri) } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Decompilation failed: ${error}`) } } - private formatDecompiledOutput(output: string, bocPath?: string): string { + private formatDecompiledOutput(output: string, bocUri: vscode.Uri): string { const header = [ "// Decompiled BOC file", "// Note: This is auto-generated code", `// Time: ${new Date().toISOString()}`, - ...(bocPath ? [`// Source: ${bocPath}`] : []), + `// Source: ${bocUri.fsPath}`, "", "", ].join("\n") @@ -76,8 +72,8 @@ export class BocDecompilerProvider implements vscode.TextDocumentContentProvider } public update(uri: vscode.Uri): void { - const bocPath = this.getBocPath(uri) - this.lastModified.set(bocPath, new Date()) + const bocUri = this.getBocPath(uri) + this.lastModified.set(bocUri, new Date()) this._onDidChange.fire(uri) } } diff --git a/client/src/providers/BocFileSystemProvider.ts b/client/src/providers/BocFileSystemProvider.ts index eb8e28ee..3b40c491 100644 --- a/client/src/providers/BocFileSystemProvider.ts +++ b/client/src/providers/BocFileSystemProvider.ts @@ -2,7 +2,6 @@ // Copyright © 2025 TON Studio import * as vscode from "vscode" import {BocDecompilerProvider} from "./BocDecompilerProvider" -import {readFileSync} from "node:fs" export class BocFileSystemProvider implements vscode.FileSystemProvider { private readonly _emitter: vscode.EventEmitter = @@ -31,7 +30,7 @@ export class BocFileSystemProvider implements vscode.FileSystemProvider { public async readFile(uri: vscode.Uri): Promise { console.log("Reading BOC file:", uri.fsPath) try { - const fileContent = readFileSync(uri.fsPath) + const fileContent = await vscode.workspace.fs.readFile(uri) console.log("File content length:", fileContent.length) const decompileUri = uri.with({ diff --git a/client/src/utils/package-manager.ts b/client/src/utils/package-manager.ts index 45d4d0ae..9d7d8d78 100644 --- a/client/src/utils/package-manager.ts +++ b/client/src/utils/package-manager.ts @@ -1,34 +1,34 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio import * as vscode from "vscode" -import * as fs from "node:fs" -import * as path from "node:path" export type PackageManager = "yarn" | "npm" | "pnpm" | "bun" -export function detectPackageManager(): PackageManager { +export async function detectPackageManager(): Promise { const workspaceFolders = vscode.workspace.workspaceFolders if (!workspaceFolders || workspaceFolders.length === 0) return "npm" - const workspaceRoot = workspaceFolders[0].uri.fsPath + const workspaceRoot = workspaceFolders[0].uri // Check for lock files - if (fs.existsSync(path.join(workspaceRoot, "bun.lockb"))) { + if (await pathExits(workspaceRoot, "bun.lockb")) { return "bun" } - if (fs.existsSync(path.join(workspaceRoot, "yarn.lock"))) { + if (await pathExits(workspaceRoot, "yarn.lock")) { return "yarn" } - if (fs.existsSync(path.join(workspaceRoot, "pnpm-lock.yaml"))) { + if (await pathExits(workspaceRoot, "pnpm-lock.yaml")) { return "pnpm" } - if (fs.existsSync(path.join(workspaceRoot, "package-lock.json"))) { + if (await pathExits(workspaceRoot, "package-lock.json")) { return "npm" } try { - const packageJsonPath = path.join(workspaceRoot, "package.json") - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { + const packageJsonContent = await vscode.workspace.fs.readFile( + vscode.Uri.joinPath(workspaceFolders[0].uri, "package.json"), + ) + const packageJson = JSON.parse(new TextDecoder().decode(packageJsonContent)) as { packageManager?: string } @@ -49,3 +49,12 @@ export function detectPackageManager(): PackageManager { return "npm" } + +async function pathExits(path: vscode.Uri, file: string): Promise { + try { + await vscode.workspace.fs.stat(vscode.Uri.joinPath(path, file)) + return true + } catch { + return false + } +} diff --git a/server/src/files.ts b/server/src/files.ts index 3348f1b6..753b1b16 100644 --- a/server/src/files.ts +++ b/server/src/files.ts @@ -3,7 +3,7 @@ import {TextDocument} from "vscode-languageserver-textdocument" import {TactFile} from "@server/languages/tact/psi/TactFile" import {pathToFileURL} from "node:url" import {createFiftParser, createTactParser, createTlbParser} from "@server/parser" -import * as fs from "node:fs" +import {readFileVFS, globalVFS} from "@server/vfs/files-adapter" import {FiftFile} from "@server/languages/fift/psi/FiftFile" import {TlbFile} from "@server/languages/tlb/psi/TlbFile" import {URI} from "vscode-uri" @@ -12,13 +12,13 @@ export const PARSED_FILES_CACHE: Map = new Map() export const FIFT_PARSED_FILES_CACHE: Map = new Map() export const TLB_PARSED_FILES_CACHE: Map = new Map() -export function findTactFile(uri: string, changed: boolean = false): TactFile { +export async function findTactFile(uri: string, changed: boolean = false): Promise { const cached = PARSED_FILES_CACHE.get(uri) if (cached !== undefined && !changed) { return cached } - const rawContent = readOrUndefined(fileURLToPath(uri)) + const rawContent = await readOrUndefined(uri) if (rawContent === undefined) { console.error(`cannot read ${uri} file`) } @@ -39,13 +39,13 @@ export function reparseTactFile(uri: string, content: string): TactFile { return file } -export function findFiftFile(uri: string): FiftFile { +export async function findFiftFile(uri: string): Promise { const cached = FIFT_PARSED_FILES_CACHE.get(uri) if (cached !== undefined) { return cached } - const rawContent = readOrUndefined(fileURLToPath(uri)) + const rawContent = await readOrUndefined(uri) if (rawContent === undefined) { console.error(`cannot read ${uri} file`) } @@ -66,13 +66,13 @@ export function reparseFiftFile(uri: string, content: string): FiftFile { return file } -export function findTlbFile(uri: string): TlbFile { +export async function findTlbFile(uri: string): Promise { const cached = TLB_PARSED_FILES_CACHE.get(uri) if (cached) { return cached } - const rawContent = readOrUndefined(fileURLToPath(uri)) + const rawContent = await readOrUndefined(uri) if (rawContent === undefined) { console.error(`cannot read ${uri} file`) } @@ -93,12 +93,12 @@ export function reparseTlbFile(uri: string, content: string): TlbFile { return file } -function readOrUndefined(path: string): string | undefined { - try { - return fs.readFileSync(path, "utf8") - } catch { - return undefined - } +async function readOrUndefined(uri: string): Promise { + return readFileVFS(globalVFS, uri) +} + +export function uriToFilePath(uri: string): string { + return fileURLToPath(uri) } export const isTactFile = ( @@ -116,6 +116,9 @@ export const isTlbFile = ( event?: lsp.TextDocumentChangeEvent, ): boolean => event?.document.languageId === "tlb" || uri.endsWith(".tlb") +// export function filePathToUri(filePath: string): string { +// return pathToFileURL(filePath).href +// } export const filePathToUri = (filePath: string): string => { const url = pathToFileURL(filePath).toString() return url.replace(/c:/g, "c%3A").replace(/d:/g, "d%3A") diff --git a/server/src/indexing-root.ts b/server/src/indexing-root.ts index 06357cbc..bf428622 100644 --- a/server/src/indexing-root.ts +++ b/server/src/indexing-root.ts @@ -58,7 +58,7 @@ export class IndexingRoot { console.info("Indexing:", filePath) const absPath = path.join(rootDir, filePath) const uri = filePathToUri(absPath) - const file = findTactFile(uri) + const file = await findTactFile(uri) index.addFile(uri, file, false) } } diff --git a/server/src/languages/fift/documentation/documentation.ts b/server/src/languages/fift/documentation/documentation.ts index 2c6e62f7..0bd2001b 100644 --- a/server/src/languages/fift/documentation/documentation.ts +++ b/server/src/languages/fift/documentation/documentation.ts @@ -8,13 +8,13 @@ import {FiftFile} from "@server/languages/fift/psi/FiftFile" const CODE_FENCE = "```" -export function generateFiftDocFor(node: SyntaxNode, file: FiftFile): string | null { +export async function generateFiftDocFor(node: SyntaxNode, file: FiftFile): Promise { const def = FiftReference.resolve(node, file) if (def) { return `${CODE_FENCE}fift\n${def.parent?.text}\n${CODE_FENCE}` } - const instr = findInstruction(node.text, []) + const instr = await findInstruction(node.text, []) if (!instr) return null const doc = generateAsmDoc(instr) diff --git a/server/src/languages/fift/documentation/index.ts b/server/src/languages/fift/documentation/index.ts index f1dba972..1ffb45bc 100644 --- a/server/src/languages/fift/documentation/index.ts +++ b/server/src/languages/fift/documentation/index.ts @@ -4,8 +4,11 @@ import {generateFiftDocFor} from "@server/languages/fift/documentation/documenta import {asLspRange} from "@server/utils/position" import {FiftFile} from "@server/languages/fift/psi/FiftFile" -export function provideFiftDocumentation(hoverNode: SyntaxNode, file: FiftFile): lsp.Hover | null { - const doc = generateFiftDocFor(hoverNode, file) +export async function provideFiftDocumentation( + hoverNode: SyntaxNode, + file: FiftFile, +): Promise { + const doc = await generateFiftDocFor(hoverNode, file) if (doc === null) return null return { diff --git a/server/src/languages/fift/inlays/collect.ts b/server/src/languages/fift/inlays/collect.ts index 857a6c9f..ad57ef6e 100644 --- a/server/src/languages/fift/inlays/collect.ts +++ b/server/src/languages/fift/inlays/collect.ts @@ -1,24 +1,24 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio import type {InlayHint} from "vscode-languageserver" -import {RecursiveVisitor} from "@server/languages/tact/psi/visitor" +import {AsyncRecursiveVisitor} from "@server/languages/tact/psi/visitor" import {findInstruction} from "@server/languages/tact/completion/data/types" import {InlayHintKind} from "vscode-languageserver-types" import {instructionPresentation} from "@server/languages/tact/asm/gas" import {FiftFile} from "@server/languages/fift/psi/FiftFile" -export function collectFift( +export async function provideFiftInlayHints( file: FiftFile, gasFormat: string, settings: { showGasConsumption: boolean }, -): InlayHint[] { +): Promise { const result: InlayHint[] = [] - RecursiveVisitor.visit(file.rootNode, (n): boolean => { + await AsyncRecursiveVisitor.visit(file.rootNode, async (n): Promise => { if (n.type === "identifier" && settings.showGasConsumption) { - const instruction = findInstruction(n.text) + const instruction = await findInstruction(n.text) if (!instruction) return true const presentation = instructionPresentation( diff --git a/server/src/languages/tact/asm/gas.ts b/server/src/languages/tact/asm/gas.ts index 426f2edc..5cc34caa 100644 --- a/server/src/languages/tact/asm/gas.ts +++ b/server/src/languages/tact/asm/gas.ts @@ -11,13 +11,13 @@ export interface GasConsumption { readonly exact: boolean } -export function computeSeqGasConsumption( +export async function computeSeqGasConsumption( arg: SyntaxNode, file: TactFile, gasSettings: { loopGasCoefficient: number }, -): GasConsumption { +): Promise { const instructions = arg.children .filter(it => it?.type === "asm_expression") .filter(it => it !== null) @@ -26,17 +26,17 @@ export function computeSeqGasConsumption( return computeGasConsumption(instructions, gasSettings) } -export function computeGasConsumption( +export async function computeGasConsumption( instructions: AsmInstr[], gasSettings: { loopGasCoefficient: number }, -): GasConsumption { +): Promise { let exact = true let res = 0 for (const instr of instructions) { - const info = instr.info() + const info = await instr.info() if (!info || info.doc.gas === "") { exact = false continue @@ -44,8 +44,8 @@ export function computeGasConsumption( const args = instr.arguments() const continuations = args.filter(it => it.type === "asm_sequence") - const continuationsGas = continuations.map(it => - computeSeqGasConsumption(it, instr.file, gasSettings), + const continuationsGas = await Promise.all( + continuations.map(async it => computeSeqGasConsumption(it, instr.file, gasSettings)), ) exact &&= continuationsGas.reduce((prev, it) => prev && it.exact, true) diff --git a/server/src/languages/tact/compiler/fmt/fmt.script.ts b/server/src/languages/tact/compiler/fmt/fmt.script.ts deleted file mode 100644 index e5e8e304..00000000 --- a/server/src/languages/tact/compiler/fmt/fmt.script.ts +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Studio -import * as fs from "node:fs" -import * as path from "node:path" -import {formatCode} from "@server/languages/tact/compiler/fmt/fmt" - -const args = process.argv.slice(2) -const firstArg = args.shift() -if (!firstArg) { - throw new Error("expected argument") -} - -if (!fs.statSync(firstArg).isFile()) { - const files = fs.globSync("**/*.tact", { - cwd: "/Users/petrmakhnev/tact", - withFileTypes: false, - exclude: (file): boolean => { - return ( - file.includes("renamer-expected") || - file.includes("test-failed") || - file === "config.tact" - ) - }, - }) - - files.forEach(file => { - const basePath = "/Users/petrmakhnev/tact" - const fullPath = path.join(basePath, file) - - if (fullPath.includes("grammar/test")) { - return - } - - const content = fs.readFileSync(fullPath, "utf8") - const result = formatCode(content) - if (result.$ === "FormatCodeError") { - console.log(result.message) - return - } - - if (result.code === content) { - // console.log("already formatted") - return - } - - console.log(`reformat ${file}`) - fs.writeFileSync(fullPath, result.code) - }) - - process.exit(0) -} - -const code = fs.readFileSync(`${process.cwd()}/${firstArg}`, "utf8") - -const result = formatCode(code) -if (result.$ === "FormatCodeError") { - throw new Error(result.message) -} - -const name = path.basename(firstArg) -fs.writeFileSync(`${name}.fmt.tact`, result.code) diff --git a/server/src/languages/tact/completion/CompletionProvider.ts b/server/src/languages/tact/completion/CompletionProvider.ts index 293f7453..08b55b61 100644 --- a/server/src/languages/tact/completion/CompletionProvider.ts +++ b/server/src/languages/tact/completion/CompletionProvider.ts @@ -7,3 +7,8 @@ export interface CompletionProvider { isAvailable(ctx: CompletionContext): boolean addCompletion(ctx: CompletionContext, result: CompletionResult): void } + +export interface AsyncCompletionProvider { + isAvailable(ctx: CompletionContext): boolean + addCompletion(ctx: CompletionContext, result: CompletionResult): Promise +} diff --git a/server/src/languages/tact/completion/data/types.ts b/server/src/languages/tact/completion/data/types.ts index 7b15f406..1e2dc6ac 100644 --- a/server/src/languages/tact/completion/data/types.ts +++ b/server/src/languages/tact/completion/data/types.ts @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio -import * as fs from "node:fs" import * as path from "node:path" import {Node as SyntaxNode} from "web-tree-sitter" +import {globalVFS, readFileVFS} from "@server/vfs/files-adapter" +import {pathToFileURL} from "node:url" export interface AsmInstruction { readonly mnemonic: string @@ -38,19 +39,28 @@ export interface AsmData { let data: AsmData | null = null -export function asmData(): AsmData { +export async function asmData(): Promise { if (data !== null) { return data } const filePath = path.join(__dirname, "asm.json") - const content = fs.readFileSync(filePath, "utf8") + const content = await readFileVFS(globalVFS, filePathToUri(filePath)) + if (content === undefined) return {instructions: [], aliases: []} data = JSON.parse(content) as AsmData return data } -export function findInstruction(name: string, args: SyntaxNode[] = []): AsmInstruction | null { - const data = asmData() +export const filePathToUri = (filePath: string): string => { + const url = pathToFileURL(filePath).toString() + return url.replace(/c:/g, "c%3A").replace(/d:/g, "d%3A") +} + +export async function findInstruction( + name: string, + args: SyntaxNode[] = [], +): Promise { + const data = await asmData() const realName = adjustName(name, args) const instruction = data.instructions.find(i => i.mnemonic === realName) diff --git a/server/src/languages/tact/completion/index.ts b/server/src/languages/tact/completion/index.ts index 92c1ed64..c8b35552 100644 --- a/server/src/languages/tact/completion/index.ts +++ b/server/src/languages/tact/completion/index.ts @@ -8,7 +8,10 @@ import {Reference} from "@server/languages/tact/psi/Reference" import {CompletionContext} from "@server/languages/tact/completion/CompletionContext" import {getDocumentSettings} from "@server/settings/settings" import {CompletionResult} from "@server/languages/tact/completion/WeightedCompletionItem" -import type {CompletionProvider} from "@server/languages/tact/completion/CompletionProvider" +import type { + AsyncCompletionProvider, + CompletionProvider, +} from "@server/languages/tact/completion/CompletionProvider" import {SnippetsCompletionProvider} from "@server/languages/tact/completion/providers/SnippetsCompletionProvider" import {KeywordsCompletionProvider} from "@server/languages/tact/completion/providers/KeywordsCompletionProvider" import {AsKeywordCompletionProvider} from "@server/languages/tact/completion/providers/AsKeywordCompletionProvider" @@ -107,7 +110,6 @@ export async function provideTactCompletion( new SnippetsCompletionProvider(), new KeywordsCompletionProvider(), new AsKeywordCompletionProvider(), - new ImportPathCompletionProvider(), new MapTypeCompletionProvider(), new BouncedTypeCompletionProvider(), new GetterCompletionProvider(), @@ -123,16 +125,24 @@ export async function provideTactCompletion( new SelfCompletionProvider(), new ReturnCompletionProvider(), new ReferenceCompletionProvider(ref), - new AsmInstructionCompletionProvider(), new PostfixCompletionProvider(), new TypeTlbSerializationCompletionProvider(), ] - providers.forEach((provider: CompletionProvider) => { - if (!provider.isAvailable(ctx)) return + for (const provider of providers) { + if (!provider.isAvailable(ctx)) continue provider.addCompletion(ctx, result) - }) + } + + const asyncProviders: AsyncCompletionProvider[] = [ + new ImportPathCompletionProvider(), + new AsmInstructionCompletionProvider(), + ] + for (const provider of asyncProviders) { + if (!provider.isAvailable(ctx)) continue + await provider.addCompletion(ctx, result) + } return result.sorted() } @@ -148,8 +158,8 @@ export async function provideTactCompletionResolve( const settings = await getDocumentSettings(data.file.uri) if (!settings.completion.addImports) return item - const file = findTactFile(data.file.uri) - const elementFile = findTactFile(data.elementFile.uri) + const file = await findTactFile(data.file.uri) + const elementFile = await findTactFile(data.elementFile.uri) // skip the same file element if (file.uri === elementFile.uri) return item diff --git a/server/src/languages/tact/completion/providers/AsmInstructionCompletionProvider.ts b/server/src/languages/tact/completion/providers/AsmInstructionCompletionProvider.ts index a55cd1f4..00c41e45 100644 --- a/server/src/languages/tact/completion/providers/AsmInstructionCompletionProvider.ts +++ b/server/src/languages/tact/completion/providers/AsmInstructionCompletionProvider.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio -import type {CompletionProvider} from "@server/languages/tact/completion/CompletionProvider" +import type {AsyncCompletionProvider} from "@server/languages/tact/completion/CompletionProvider" import {CompletionItemKind} from "vscode-languageserver-types" import type {CompletionContext} from "@server/languages/tact/completion/CompletionContext" import {asmData, getStackPresentation} from "@server/languages/tact/completion/data/types" @@ -9,13 +9,13 @@ import { CompletionWeight, } from "@server/languages/tact/completion/WeightedCompletionItem" -export class AsmInstructionCompletionProvider implements CompletionProvider { +export class AsmInstructionCompletionProvider implements AsyncCompletionProvider { public isAvailable(ctx: CompletionContext): boolean { return ctx.element.node.type === "tvm_instruction" } - public addCompletion(_ctx: CompletionContext, result: CompletionResult): void { - const data = asmData() + public async addCompletion(_ctx: CompletionContext, result: CompletionResult): Promise { + const data = await asmData() for (const instruction of data.instructions) { const name = this.adjustName(instruction.mnemonic) diff --git a/server/src/languages/tact/completion/providers/ImportPathCompletionProvider.ts b/server/src/languages/tact/completion/providers/ImportPathCompletionProvider.ts index 6c813d46..445e1a55 100644 --- a/server/src/languages/tact/completion/providers/ImportPathCompletionProvider.ts +++ b/server/src/languages/tact/completion/providers/ImportPathCompletionProvider.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio -import type {CompletionProvider} from "@server/languages/tact/completion/CompletionProvider" +import {AsyncCompletionProvider} from "@server/languages/tact/completion/CompletionProvider" import {CompletionItemKind} from "vscode-languageserver-types" import type {CompletionContext} from "@server/languages/tact/completion/CompletionContext" import { @@ -8,17 +8,19 @@ import { CompletionWeight, } from "@server/languages/tact/completion/WeightedCompletionItem" import * as path from "node:path" -import * as fs from "node:fs" +import {globalVFS} from "@server/vfs/global" +import {listDirs, listFiles} from "@server/vfs/vfs" +import {filePathToUri} from "@server/files" import {TactFile} from "@server/languages/tact/psi/TactFile" import {projectStdlibPath} from "@server/languages/tact/toolchain/toolchain" import {trimSuffix} from "@server/utils/strings" -export class ImportPathCompletionProvider implements CompletionProvider { +export class ImportPathCompletionProvider implements AsyncCompletionProvider { public isAvailable(ctx: CompletionContext): boolean { return ctx.insideImport } - public addCompletion(ctx: CompletionContext, result: CompletionResult): void { + public async addCompletion(ctx: CompletionContext, result: CompletionResult): Promise { const file = ctx.element.file const currentDir = path.dirname(file.path) @@ -26,19 +28,19 @@ export class ImportPathCompletionProvider implements CompletionProvider { if (importPath.startsWith("@stdlib/") && projectStdlibPath) { const libsDir = path.join(projectStdlibPath, "libs") - this.addEntries(libsDir, file, "", result) + await this.addEntries(libsDir, file, "", result) return } if (importPath.startsWith("./") || importPath.startsWith("../")) { const targetDir = path.join(currentDir, importPath) - this.addEntries(targetDir, file, "", result) + await this.addEntries(targetDir, file, "", result) return } // On empty path: // import ""; - this.addEntries(currentDir, file, "./", result) + await this.addEntries(currentDir, file, "./", result) result.add({ label: "@stdlib/", @@ -47,23 +49,25 @@ export class ImportPathCompletionProvider implements CompletionProvider { }) } - private addEntries( + private async addEntries( dir: string, file: TactFile, prefix: string, result: CompletionResult, - ): void { - this.files(dir, file).forEach(name => { + ): Promise { + const files = await this.files(dir, file) + for (const name of files) { this.addFile(`${prefix}${name}`, result) - }) + } - this.dirs(dir).forEach(name => { + const dirs = await this.dirs(dir) + for (const name of dirs) { result.add({ label: name + "/", kind: CompletionItemKind.Folder, weight: CompletionWeight.CONTEXT_ELEMENT, }) - }) + } } private addFile(name: string, result: CompletionResult): void { @@ -77,15 +81,23 @@ export class ImportPathCompletionProvider implements CompletionProvider { }) } - private files(dir: string, currentFile: TactFile): string[] { - return fs - .readdirSync(dir) - .filter(file => file.endsWith(".tact")) - .map(file => path.basename(file, ".tact")) - .filter(name => name !== currentFile.name) + private async files(dir: string, currentFile: TactFile): Promise { + try { + const allFiles = await listFiles(globalVFS, filePathToUri(dir)) + return allFiles + .filter(file => file.endsWith(".tact")) + .map(file => path.basename(file, ".tact")) + .filter(name => name !== currentFile.name) + } catch { + return [] + } } - private dirs(dir: string): string[] { - return fs.readdirSync(dir).filter(file => fs.lstatSync(path.join(dir, file)).isDirectory()) + private async dirs(dir: string): Promise { + try { + return await listDirs(globalVFS, filePathToUri(dir)) + } catch { + return [] + } } } diff --git a/server/src/languages/tact/custom/selection-gas-consumption.ts b/server/src/languages/tact/custom/selection-gas-consumption.ts index 6eaddd57..123dd278 100644 --- a/server/src/languages/tact/custom/selection-gas-consumption.ts +++ b/server/src/languages/tact/custom/selection-gas-consumption.ts @@ -18,7 +18,7 @@ export async function provideSelectionGasConsumption( ): Promise { try { const uri = params.textDocument.uri - const file = findTactFile(uri) + const file = await findTactFile(uri) const startPoint = asParserPoint(params.range.start) const endPoint = asParserPoint(params.range.end) @@ -61,7 +61,7 @@ export async function provideSelectionGasConsumption( } const settings = await getDocumentSettings(uri) - const gasConsumption = computeGasConsumption(selectedInstructions, settings.gas) + const gasConsumption = await computeGasConsumption(selectedInstructions, settings.gas) return { gasConsumption: { diff --git a/server/src/languages/tact/documentation/documentation.ts b/server/src/languages/tact/documentation/documentation.ts index 080603c0..54ce76f1 100644 --- a/server/src/languages/tact/documentation/documentation.ts +++ b/server/src/languages/tact/documentation/documentation.ts @@ -112,7 +112,7 @@ export async function generateDocFor(node: NamedNode, place: SyntaxNode): Promis const func = new Fun(astNode, node.file) const doc = node.documentation() - const gas = func.computeGasConsumption(settings.gas) + const gas = await func.computeGasConsumption(settings.gas) const presentation = gas.exact ? gas.value.toString() : `~${gas.value}` const gasPresentation = gas.unknown ? "" : `Gas: \`${presentation}\`` diff --git a/server/src/languages/tact/documentation/index.ts b/server/src/languages/tact/documentation/index.ts index 9f317e22..0126ac6d 100644 --- a/server/src/languages/tact/documentation/index.ts +++ b/server/src/languages/tact/documentation/index.ts @@ -72,7 +72,7 @@ export async function provideTactDocumentation( if (!asmExpression) return null const instr = new AsmInstr(asmExpression, file) - const info = instr.info() + const info = await instr.info() if (!info) return null const doc = generateAsmDoc(info) diff --git a/server/src/languages/tact/inlays/index.ts b/server/src/languages/tact/inlays/index.ts index f36cd53f..68701047 100644 --- a/server/src/languages/tact/inlays/index.ts +++ b/server/src/languages/tact/inlays/index.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio import {InlayHint, InlayHintKind} from "vscode-languageserver-types" -import {RecursiveVisitor} from "@server/languages/tact/psi/visitor" +import {AsyncRecursiveVisitor} from "@server/languages/tact/psi/visitor" import type {TactFile} from "@server/languages/tact/psi/TactFile" import {TypeInferer} from "@server/languages/tact/TypeInferer" import {Reference} from "@server/languages/tact/psi/Reference" @@ -165,7 +165,7 @@ function hasObviousType(expr: SyntaxNode): boolean { return false } -export function collectTactInlays( +export async function collectTactInlays( file: TactFile, hints: { types: boolean @@ -187,7 +187,7 @@ export function collectTactInlays( gasSettings: { loopGasCoefficient: number }, -): InlayHint[] | null { +): Promise { if (!hints.types && !hints.parameters) return [] const result: InlayHint[] = [] @@ -197,7 +197,7 @@ export function collectTactInlays( value: "Note that this value is approximate!\n\nLearn more about how LS calculates this: https://github.com/tact-lang/tact-language-server/blob/master/docs/manual/features/gas-calculation.md", } - RecursiveVisitor.visit(file.rootNode, (n): boolean => { + await AsyncRecursiveVisitor.visit(file.rootNode, async (n): Promise => { const type = n.type if (type === "let_statement" && hints.types) { @@ -469,7 +469,7 @@ export function collectTactInlays( if (!openBrace || !closeBrace) return true if (openBrace.startPosition.row === closeBrace.startPosition.row) return true - const gas = computeSeqGasConsumption(n, file, gasSettings) + const gas = await computeSeqGasConsumption(n, file, gasSettings) result.push({ kind: InlayHintKind.Type, @@ -485,7 +485,7 @@ export function collectTactInlays( if (type === "asm_expression" && hints.showAsmInstructionGas) { const instr = new AsmInstr(n, file) - const info = instr.info() + const info = await instr.info() const presentation = instructionPresentation( info?.doc.gas, @@ -512,7 +512,7 @@ export function collectTactInlays( if (!openBrace || !closeBrace) return true if (openBrace.startPosition.row === closeBrace.startPosition.row) return true - const gas = func.computeGasConsumption(gasSettings) + const gas = await func.computeGasConsumption(gasSettings) if (gas.unknown || gas.value === 0) { console.log("here", gas) return true diff --git a/server/src/languages/tact/inspections/CompilerInspection.ts b/server/src/languages/tact/inspections/CompilerInspection.ts index c4269a59..2205efa8 100644 --- a/server/src/languages/tact/inspections/CompilerInspection.ts +++ b/server/src/languages/tact/inspections/CompilerInspection.ts @@ -7,7 +7,8 @@ import {Inspection, InspectionIds} from "./Inspection" import {URI} from "vscode-uri" import {workspaceRoot} from "@server/toolchain" import * as path from "node:path" -import {existsSync} from "node:fs" +import {existsVFS, globalVFS} from "@server/vfs/files-adapter" +import {filePathToUri} from "@server/files" export class CompilerInspection implements Inspection { public readonly id: "tact-compiler-errors" = InspectionIds.COMPILER @@ -16,7 +17,7 @@ export class CompilerInspection implements Inspection { if (file.fromStdlib) return [] const configPath = path.join(workspaceRoot, "tact.config.json") - const hasConfig = existsSync(configPath) + const hasConfig = await existsVFS(globalVFS, filePathToUri(configPath)) try { const filePath = URI.parse(file.uri).fsPath diff --git a/server/src/languages/tact/inspections/MistInspection.ts b/server/src/languages/tact/inspections/MistInspection.ts index 330b9129..666e9a5d 100644 --- a/server/src/languages/tact/inspections/MistInspection.ts +++ b/server/src/languages/tact/inspections/MistInspection.ts @@ -8,8 +8,9 @@ import {Severity} from "@server/languages/tact/compiler/TactCompiler" import {Inspection, InspectionIds} from "@server/languages/tact/inspections/Inspection" import * as path from "node:path" import {workspaceRoot} from "@server/toolchain" -import {existsSync} from "node:fs" import {getDocumentSettings} from "@server/settings/settings" +import {existsVFS, globalVFS} from "@server/vfs/files-adapter" +import {filePathToUri} from "@server/files" export class MistiInspection implements Inspection { public readonly id: "misti" = InspectionIds.MISTI @@ -18,7 +19,7 @@ export class MistiInspection implements Inspection { if (file.fromStdlib) return [] const configPath = path.join(workspaceRoot, "tact.config.json") - const hasConfig = existsSync(configPath) + const hasConfig = await existsVFS(globalVFS, filePathToUri(configPath)) const settings = await getDocumentSettings(file.uri) diff --git a/server/src/languages/tact/intentions/ExtractToFile.ts b/server/src/languages/tact/intentions/ExtractToFile.ts index 48048e96..92548ac3 100644 --- a/server/src/languages/tact/intentions/ExtractToFile.ts +++ b/server/src/languages/tact/intentions/ExtractToFile.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Studio -import type {Intention, IntentionContext} from "@server/languages/tact/intentions/Intention" +import {AsyncIntention, IntentionContext} from "@server/languages/tact/intentions/Intention" import type {WorkspaceEdit} from "vscode-languageserver" import type {TactFile} from "@server/languages/tact/psi/TactFile" import {asLspRange, asParserPoint} from "@server/utils/position" @@ -20,10 +20,10 @@ import { import * as path from "node:path" import {fileURLToPath} from "node:url" import * as lsp from "vscode-languageserver" -import {existsSync} from "node:fs" import {filePathToUri} from "@server/files" +import {existsVFS, globalVFS} from "@server/vfs/files-adapter" -export class ExtractToFile implements Intention { +export class ExtractToFile implements AsyncIntention { public readonly id: string = "tact.extract-to-file" public readonly name: string = "Extract to new file..." @@ -116,7 +116,7 @@ export class ExtractToFile implements Intention { return false } - public invoke(ctx: IntentionContext): WorkspaceEdit | null { + public async invoke(ctx: IntentionContext): Promise { const element = ExtractToFile.findExtractableElement(ctx) if (!element) return null @@ -127,18 +127,18 @@ export class ExtractToFile implements Intention { return null } - private performExtraction( + private async performExtraction( ctx: IntentionContext, element: NamedNode, customFileName: string, - ): WorkspaceEdit | null { + ): Promise { const elementText = element.node.text const newFileUri = this.generateFileUri(customFileName, ctx.file) const newFileContent = this.generateFileContent(elementText) const documentChanges: (lsp.TextDocumentEdit | lsp.CreateFile)[] = [] const newFilePath = fileURLToPath(newFileUri) - const fileExists = existsSync(newFilePath) + const fileExists = await existsVFS(globalVFS, newFilePath) if (fileExists) { documentChanges.push({ diff --git a/server/src/languages/tact/intentions/Intention.ts b/server/src/languages/tact/intentions/Intention.ts index 5c9bb8fd..1d54b86b 100644 --- a/server/src/languages/tact/intentions/Intention.ts +++ b/server/src/languages/tact/intentions/Intention.ts @@ -28,3 +28,12 @@ export interface Intention { invoke(ctx: IntentionContext): WorkspaceEdit | null } + +export interface AsyncIntention { + readonly id: string + readonly name: string + + isAvailable(ctx: IntentionContext): boolean + + invoke(ctx: IntentionContext): Promise +} diff --git a/server/src/languages/tact/intentions/index.ts b/server/src/languages/tact/intentions/index.ts index 44c77096..08393f3a 100644 --- a/server/src/languages/tact/intentions/index.ts +++ b/server/src/languages/tact/intentions/index.ts @@ -1,4 +1,5 @@ import type { + AsyncIntention, Intention, IntentionArguments, IntentionContext, @@ -36,9 +37,10 @@ export const INTENTIONS: Intention[] = [ new WrapSelectedToTry(), new WrapSelectedToTryCatch(), new WrapSelectedToRepeat(), - new ExtractToFile(), ] +export const ASYNC_INTENTIONS: AsyncIntention[] = [new ExtractToFile()] + export async function provideExecuteTactCommand( params: lsp.ExecuteCommandParams, ): Promise { @@ -66,12 +68,14 @@ export async function provideExecuteTactCommand( if (!params.arguments || params.arguments.length === 0) return null - const intention = INTENTIONS.find(it => it.id === params.command) + const intention = + INTENTIONS.find(it => it.id === params.command) ?? + ASYNC_INTENTIONS.find(it => it.id === params.command) if (!intention) return null const args = params.arguments[0] as IntentionArguments - const file = findTactFile(args.fileUri) + const file = await findTactFile(args.fileUri) const ctx: IntentionContext = { file: file, @@ -140,7 +144,7 @@ export function provideTactCodeActions( const actions: lsp.CodeAction[] = [] - for (const intention of INTENTIONS) { + for (const intention of [...INTENTIONS, ...ASYNC_INTENTIONS]) { if (!intention.isAvailable(ctx)) continue actions.push({ diff --git a/server/src/languages/tact/psi/Decls.ts b/server/src/languages/tact/psi/Decls.ts index afe52103..70f37af8 100644 --- a/server/src/languages/tact/psi/Decls.ts +++ b/server/src/languages/tact/psi/Decls.ts @@ -571,7 +571,7 @@ export class Fun extends NamedNode { return "0x" + ((crc16(Buffer.from(this.name())) & 0xff_ff) | 0x1_00_00).toString(16) } - public computeGasConsumption(gas: {loopGasCoefficient: number}): GasConsumption { + public async computeGasConsumption(gas: {loopGasCoefficient: number}): Promise { const body = this.node.childForFieldName("body") if (!body) { return { diff --git a/server/src/languages/tact/psi/ImportResolver.ts b/server/src/languages/tact/psi/ImportResolver.ts index a6390b1f..a694e300 100644 --- a/server/src/languages/tact/psi/ImportResolver.ts +++ b/server/src/languages/tact/psi/ImportResolver.ts @@ -3,7 +3,6 @@ import type {Node as SyntaxNode} from "web-tree-sitter" import * as path from "node:path" import type {TactFile} from "./TactFile" -import {existsSync} from "node:fs" import {trimPrefix, trimSuffix} from "@server/utils/strings" import {projectStdlibPath} from "@server/languages/tact/toolchain/toolchain" import {filePathToUri, PARSED_FILES_CACHE} from "@server/files" @@ -48,7 +47,12 @@ export class ImportResolver { } private static checkFile(targetPath: string, check: boolean): string | null { - if (check && !existsSync(targetPath)) return null + if (check) { + // TODO + // const targetUri = filePathToUri(targetPath) + // const exists = await existsVFS(globalVFS, targetUri) + // if (!exists) return null + } return targetPath } diff --git a/server/src/languages/tact/psi/TactNode.ts b/server/src/languages/tact/psi/TactNode.ts index 5bdd120f..0efa996f 100644 --- a/server/src/languages/tact/psi/TactNode.ts +++ b/server/src/languages/tact/psi/TactNode.ts @@ -202,7 +202,7 @@ export class AsmInstr extends NamedNode { return argsList.children.filter(it => it !== null) } - public info(): AsmInstruction | null { + public async info(): Promise { return findInstruction(this.name(), this.arguments()) } } diff --git a/server/src/languages/tact/psi/visitor.ts b/server/src/languages/tact/psi/visitor.ts index e59b6d93..5d7c08a8 100644 --- a/server/src/languages/tact/psi/visitor.ts +++ b/server/src/languages/tact/psi/visitor.ts @@ -59,3 +59,26 @@ export class RecursiveVisitor { return } } + +export class AsyncRecursiveVisitor { + public static async visit( + node: SyntaxNode | null, + cb: (n: SyntaxNode) => Promise, + ): Promise { + if (!node) return + + const walker = new TreeWalker(node.walk()) + let current: SyntaxNode | null = node + + while (current) { + const result = await cb(current) + if (result === "stop") return + if (!result) { + walker.skipChildren() + } + current = walker.next() + } + + return + } +} diff --git a/server/src/languages/tact/rename/file-renaming.ts b/server/src/languages/tact/rename/file-renaming.ts index 6ed41c80..0c744720 100644 --- a/server/src/languages/tact/rename/file-renaming.ts +++ b/server/src/languages/tact/rename/file-renaming.ts @@ -9,11 +9,13 @@ import {TextEdit} from "vscode-languageserver-types" import {index} from "@server/languages/tact/indexes" import {filePathToUri, findTactFile, PARSED_FILES_CACHE} from "@server/files" -export function processFileRenaming(params: RenameFilesParams): lsp.WorkspaceEdit | null { +export async function processFileRenaming( + params: RenameFilesParams, +): Promise { const changes: Record = {} for (const fileRename of params.files) { - processFileRename(fileRename, changes) + await processFileRename(fileRename, changes) } return Object.keys(changes).length > 0 ? {changes} : null @@ -42,7 +44,10 @@ export function onFileRenamed(params: RenameFilesParams): void { } } -function processFileRename(fileRename: lsp.FileRename, changes: Record): void { +async function processFileRename( + fileRename: lsp.FileRename, + changes: Record, +): Promise { const oldUri = fileRename.oldUri const newUri = fileRename.newUri @@ -69,7 +74,7 @@ function processFileRename(fileRename: lsp.FileRename, changes: Record { + const file = await findTactFile(params.textDocument.uri) const hoverNode = nodeAtPosition(params, file) if (!hoverNode) return null diff --git a/server/src/languages/tact/toolchain/toolchain.ts b/server/src/languages/tact/toolchain/toolchain.ts index 4ee489ea..d684333d 100644 --- a/server/src/languages/tact/toolchain/toolchain.ts +++ b/server/src/languages/tact/toolchain/toolchain.ts @@ -3,10 +3,11 @@ import * as path from "node:path" import * as cp from "node:child_process" import {SpawnSyncReturns} from "node:child_process" -import {existsSync} from "node:fs" import * as console from "node:console" import * as os from "node:os" import {EnvironmentInfo, ToolchainInfo} from "@shared/shared-msgtypes" +import {existsVFS, globalVFS} from "@server/vfs/files-adapter" +import {filePathToUri} from "@server/files" export class InvalidToolchainError extends Error { public constructor(message: string) { @@ -38,15 +39,15 @@ export class Toolchain { } } - public static autoDetect(root: string): Toolchain { - const candidatesPath = [ + public static async autoDetect(root: string): Promise { + const candidatesPaths = [ path.join(root, "node_modules", ".bin", "tact"), path.join(root, "bin", "tact.js"), // path in compiler repo ] - const foundPath = candidatesPath.find(it => existsSync(it)) + const foundPath = await Toolchain.findDirectory(candidatesPaths) if (!foundPath) { console.info(`cannot find toolchain in:`) - candidatesPath.forEach(it => { + candidatesPaths.forEach(it => { console.info(it) }) return fallbackToolchain @@ -142,6 +143,16 @@ export class Toolchain { public toString(): string { return `Toolchain(path=${this.compilerPath}, version=${this.version.number}:${this.version.commit})` } + + private static async findDirectory(dir: string[]): Promise { + for (const searchDir of dir) { + if (await existsVFS(globalVFS, filePathToUri(searchDir))) { + return searchDir + } + } + + return null + } } export let projectStdlibPath: string | null = null diff --git a/server/src/server.ts b/server/src/server.ts index 0cb58d8d..06829840 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -10,7 +10,8 @@ import * as lsp from "vscode-languageserver" import {DidChangeWatchedFilesParams, FileChangeType} from "vscode-languageserver" import {TypeBasedSearch} from "@server/languages/tact/search/TypeBasedSearch" import * as path from "node:path" -import {existsSync} from "node:fs" +import {globalVFS} from "@server/vfs/global" +import {existsVFS} from "@server/vfs/files-adapter" import type {ClientOptions} from "@shared/config-scheme" import { DocumentationAtPositionRequest, @@ -30,7 +31,7 @@ import {IndexingRoot, IndexingRootKind} from "./indexing-root" import {clearDocumentSettings, getDocumentSettings, TactSettings} from "@server/settings/settings" import {provideFiftFoldingRanges} from "@server/languages/fift/foldings/collect" import {provideFiftSemanticTokens as provideFiftSemanticTokens} from "server/src/languages/fift/semantic-tokens" -import {collectFift as collectFiftInlays} from "@server/languages/fift/inlays/collect" +import {provideFiftInlayHints as collectFiftInlays} from "@server/languages/fift/inlays/collect" import {WorkspaceEdit} from "vscode-languageserver-types" import type {Node as SyntaxNode} from "web-tree-sitter" import { @@ -80,6 +81,7 @@ import { provideTactWorkspaceSymbols, } from "@server/languages/tact/symbols" import { + ASYNC_INTENTIONS, INTENTIONS, provideExecuteTactCommand, provideTactCodeActions, @@ -140,15 +142,15 @@ async function handleFileOpen( } if (isFiftFile(uri, event)) { - findFiftFile(uri) + await findFiftFile(uri) } if (isTlbFile(uri, event)) { - findTlbFile(uri) + await findTlbFile(uri) } if (isTactFile(uri, event)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) index.addFile(uri, file) if (initializationFinished) { @@ -164,7 +166,7 @@ const showErrorMessage = (msg: string): void => { }) } -function findStdlib(settings: TactSettings, rootDir: string): string | null { +async function findStdlib(settings: TactSettings, rootDir: string): Promise { if (settings.stdlib.path !== null && settings.stdlib.path.length > 0) { return settings.stdlib.path } @@ -185,10 +187,17 @@ function findStdlib(settings: TactSettings, rootDir: string): string | null { "stdlib", ] - const localFolder = - searchDirs.find(searchDir => { - return existsSync(path.join(rootDir, searchDir)) - }) ?? null + async function findDirectory(): Promise { + for (const searchDir of searchDirs) { + if (await existsVFS(globalVFS, filePathToUri(path.join(rootDir, searchDir)))) { + return searchDir + } + } + + return null + } + + const localFolder = await findDirectory() if (localFolder === null) { console.error( @@ -235,7 +244,7 @@ async function initialize(): Promise { try { toolchainManager.setWorkspaceRoot(rootDir) - toolchainManager.setToolchains( + await toolchainManager.setToolchains( settings.toolchain.toolchains, settings.toolchain.activeToolchain, ) @@ -262,7 +271,7 @@ async function initialize(): Promise { } } - const stdlibPath = findStdlib(settings, rootDir) + const stdlibPath = await findStdlib(settings, rootDir) if (stdlibPath !== null) { reporter.report(50, "Indexing: (1/3) Standard Library") const stdlibUri = filePathToUri(stdlibPath) @@ -313,13 +322,15 @@ connection.onInitialized(async () => { await initialize() }) -function findConfigFileDir(startPath: string, fileName: string): string | null { +async function findConfigFileDir(startPath: string, fileName: string): Promise { let currentPath = startPath // search only at depths up to 20 for (let i = 0; i < 20; i++) { const potentialPath = path.join(currentPath, fileName) - if (existsSync(potentialPath)) return currentPath + const potentialUri = filePathToUri(potentialPath) + const exists = await existsVFS(globalVFS, potentialUri) + if (exists) return currentPath const parentPath = path.dirname(currentPath) if (parentPath === currentPath) break @@ -335,7 +346,7 @@ function findConfigFileDir(startPath: string, fileName: string): string | null { async function initializeFallback(uri: string): Promise { // let's try to initialize with this way const filepath = fileURLToPath(uri) - const projectDir = findConfigFileDir(path.dirname(filepath), "tact.config.json") + const projectDir = await findConfigFileDir(path.dirname(filepath), "tact.config.json") if (projectDir === null) { console.info(`project directory not found, using file directory`) const dir = path.dirname(filepath) @@ -423,20 +434,21 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + connection.onDidChangeWatchedFiles(async (params: DidChangeWatchedFilesParams) => { for (const change of params.changes) { const uri = change.uri if (!isTactFile(uri)) continue if (change.type === FileChangeType.Created) { console.info(`Find external create of ${uri}`) - const file = findTactFile(uri) + const file = await findTactFile(uri) index.addFile(uri, file) continue } @@ -449,7 +461,7 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.DefinitionParams): Promise => { const uri = params.textDocument.uri if (isFiftFile(uri)) { - const file = findFiftFile(uri) + const file = await findFiftFile(uri) const node = nodeAtPosition(params, file) if (!node || node.type !== "identifier") return [] @@ -552,7 +564,7 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async ( + params: lsp.TypeDefinitionParams, + ): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) const hoverNode = nodeAtPosition(params, file) if (!hoverNode) return [] @@ -595,12 +609,12 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async ( + params: lsp.ImplementationParams, + ): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) const elementNode = nodeAtPosition(params, file) if (!elementNode) return [] return provideTactImplementations(elementNode, file) @@ -649,11 +665,11 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.RenameParams): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) return provideTactRename(params, file) } @@ -663,11 +679,11 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.PrepareRenameParams): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) const result = provideTactRenamePrepare(params, file) if (typeof result === "string") { @@ -684,11 +700,11 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.DocumentHighlightParams): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) const node = nodeAtPosition(params, file) if (!node) return null return provideTactDocumentHighlight(node, file) @@ -704,21 +720,21 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.SignatureHelpParams): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { @@ -743,16 +759,16 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.FoldingRangeParams): Promise => { const uri = params.textDocument.uri if (isFiftFile(uri)) { - const file = findFiftFile(uri) + const file = await findFiftFile(uri) return provideFiftFoldingRanges(file) } if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) return provideTactFoldingRanges(file) } @@ -767,17 +783,17 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.CodeActionParams): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) return provideTactCodeActions(file, params) } @@ -827,12 +843,12 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: lsp.DocumentFormattingParams): Promise => { const uri = params.textDocument.uri if (isFiftFile(uri) || isTlbFile(uri)) { return null } - const file = findTactFile(uri) + const file = await findTactFile(uri) const formatted = formatCode(file.content) if (formatted.$ === "FormattedCode") { @@ -893,11 +909,11 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + async (params: TypeAtPositionParams): Promise => { const uri = params.textDocument.uri if (isTactFile(uri)) { - const file = findTactFile(uri) + const file = await findTactFile(uri) return provideTactTypeAtPosition(params, file) } @@ -972,7 +988,11 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise it.id)], + commands: [ + "tact/executeGetScopeProvider", + ...INTENTIONS.map(it => it.id), + ...ASYNC_INTENTIONS.map(it => it.id), + ], }, workspace: { workspaceFolders: { diff --git a/server/src/toolchain-manager.ts b/server/src/toolchain-manager.ts index 488be702..90f1e060 100644 --- a/server/src/toolchain-manager.ts +++ b/server/src/toolchain-manager.ts @@ -22,17 +22,17 @@ export function setWorkspaceRoot(root: string): void { } } -export function setToolchains( +export async function setToolchains( toolchainConfigs: Record, activeId: string, -): void { +): Promise { const newToolchains: Map = new Map() for (const [id, config] of Object.entries(toolchainConfigs)) { try { const toolchain = id === "auto" && config.path === "" - ? Toolchain.autoDetect(state.workspaceRoot ?? process.cwd()) + ? await Toolchain.autoDetect(state.workspaceRoot ?? process.cwd()) : Toolchain.fromPath(config.path) newToolchains.set(id, toolchain) diff --git a/server/src/vfs/files-adapter.ts b/server/src/vfs/files-adapter.ts new file mode 100644 index 00000000..4e7493fd --- /dev/null +++ b/server/src/vfs/files-adapter.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio +import {VFS, readFile, exists} from "./vfs" + +export {globalVFS} from "./global" + +export async function readFileVFS(vfs: VFS, uri: string): Promise { + try { + const file = await readFile(vfs, uri) + return file?.exists ? file.content : undefined + } catch { + return undefined + } +} + +export async function existsVFS(vfs: VFS, uri: string): Promise { + try { + return await exists(vfs, uri) + } catch { + return false + } +} diff --git a/server/src/vfs/fs-provider.ts b/server/src/vfs/fs-provider.ts new file mode 100644 index 00000000..0888a93e --- /dev/null +++ b/server/src/vfs/fs-provider.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio + +/* eslint-disable @typescript-eslint/require-await */ +import {readFileSync, existsSync, readdirSync, statSync} from "node:fs" +import {join} from "node:path" +import {fileURLToPath} from "node:url" +import {FileSystemProvider, VirtualFile} from "./types" + +export function createNodeFSProvider(): FileSystemProvider { + return { + async readFile(uri: string): Promise { + try { + const filePath = fileURLToPath(uri) + const content = readFileSync(filePath, "utf8") + + return { + uri, + content, + exists: true, + } + } catch { + return { + uri, + content: "", + exists: false, + } + } + }, + + async exists(uri: string): Promise { + try { + const filePath = fileURLToPath(uri) + return existsSync(filePath) + } catch { + return false + } + }, + + async listFiles(uri: string): Promise { + try { + const dirPath = fileURLToPath(uri) + const entries = readdirSync(dirPath) + + const files: string[] = [] + + for (const entry of entries) { + const fullPath = join(dirPath, entry) + const stat = statSync(fullPath) + + if (stat.isFile()) { + files.push(entry) + } + } + + return files + } catch { + return [] + } + }, + + async listDirs(uri: string): Promise { + try { + const dirPath = fileURLToPath(uri) + const entries = readdirSync(dirPath) + + const files: string[] = [] + + for (const entry of entries) { + const fullPath = join(dirPath, entry) + const stat = statSync(fullPath) + + if (stat.isDirectory()) { + files.push(entry) + } + } + + return files + } catch { + return [] + } + }, + } +} diff --git a/server/src/vfs/global.ts b/server/src/vfs/global.ts new file mode 100644 index 00000000..395af1c9 --- /dev/null +++ b/server/src/vfs/global.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio +import {createVFS, createDefaultProvider, VFS} from "./index" + +export const globalVFS: VFS = createVFS(createDefaultProvider()) diff --git a/server/src/vfs/index.ts b/server/src/vfs/index.ts new file mode 100644 index 00000000..197543de --- /dev/null +++ b/server/src/vfs/index.ts @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio + +export type {FileSystemProvider, VirtualFile} from "./types" + +export {createVFS, readFile, exists, listFiles} from "./vfs" +export type {VFS} from "./vfs" + +export {createNodeFSProvider} from "./fs-provider" +export {createVSCodeProvider} from "./vscode-provider" + +import {FileSystemProvider} from "./types" +import {createNodeFSProvider} from "./fs-provider" +import {createVSCodeProvider} from "./vscode-provider" + +export function createDefaultProvider(): FileSystemProvider { + return typeof process !== "undefined" && process.versions.node + ? createNodeFSProvider() + : createVSCodeProvider() +} diff --git a/server/src/vfs/types.ts b/server/src/vfs/types.ts new file mode 100644 index 00000000..8adbe9f5 --- /dev/null +++ b/server/src/vfs/types.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio + +export interface VirtualFile { + readonly uri: string + readonly content: string + readonly exists: boolean +} + +export interface FileSystemProvider { + readFile(uri: string): Promise + exists(uri: string): Promise + listFiles(uri: string): Promise + listDirs(uri: string): Promise +} diff --git a/server/src/vfs/vfs.ts b/server/src/vfs/vfs.ts new file mode 100644 index 00000000..81ec4882 --- /dev/null +++ b/server/src/vfs/vfs.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio +import {FileSystemProvider, VirtualFile} from "./types" + +export interface VFS { + readonly provider: FileSystemProvider +} + +export function createVFS(provider: FileSystemProvider): VFS { + return {provider} +} + +export async function readFile(vfs: VFS, uri: string): Promise { + return vfs.provider.readFile(uri) +} + +export async function exists(vfs: VFS, uri: string): Promise { + return vfs.provider.exists(uri) +} + +export async function listFiles(vfs: VFS, uri: string): Promise { + return vfs.provider.listFiles(uri) +} + +export async function listDirs(vfs: VFS, uri: string): Promise { + return vfs.provider.listDirs(uri) +} diff --git a/server/src/vfs/vscode-provider.ts b/server/src/vfs/vscode-provider.ts new file mode 100644 index 00000000..b95b1128 --- /dev/null +++ b/server/src/vfs/vscode-provider.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Studio + +/* eslint-disable @typescript-eslint/require-await */ +import {FileSystemProvider, VirtualFile} from "./types" + +export function createVSCodeProvider(): FileSystemProvider { + return { + async readFile(_uri: string): Promise { + return null + }, + async exists(_uri: string): Promise { + return false + }, + async listFiles(_uri: string): Promise { + return [] + }, + async listDirs(_uri: string): Promise { + return [] + }, + } +}