Skip to content
Merged

VFS #720

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 28 additions & 27 deletions client/src/build-system.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>

readonly taskType: string
}
Expand All @@ -26,13 +23,13 @@ export class BlueprintTaskProvider implements TaskProviderBase {
this.group = group
}

public provideTasks(): vscode.Task[] {
const isAvailable = this.isAvailable()
public async provideTasks(): Promise<vscode.Task[]> {
const isAvailable = await this.isAvailable()
if (!isAvailable) return []
return [this.createTask()]
}

public isAvailable(): boolean {
public async isAvailable(): Promise<boolean> {
return projectUsesBlueprint()
}

Expand All @@ -45,7 +42,7 @@ export class BlueprintTaskProvider implements TaskProviderBase {
}

public createTask(): vscode.Task {
const definition: TaskDefinition = {
const definition: vscode.TaskDefinition = {
type: this.taskType,
}

Expand Down Expand Up @@ -88,12 +85,12 @@ export class TactTemplateTaskProvider implements TaskProviderBase {
this.group = group
}

public isAvailable(): boolean {
return !projectUsesBlueprint()
public async isAvailable(): Promise<boolean> {
return !(await projectUsesBlueprint())
}

public provideTasks(): vscode.Task[] {
const isAvailable = this.isAvailable()
public async provideTasks(): Promise<vscode.Task[]> {
const isAvailable = await this.isAvailable()
if (!isAvailable) return []
return [this.createTask()]
}
Expand All @@ -107,7 +104,7 @@ export class TactTemplateTaskProvider implements TaskProviderBase {
}

public createTask(): vscode.Task {
const definition: TaskDefinition = {
const definition: vscode.TaskDefinition = {
type: this.taskType,
}

Expand Down Expand Up @@ -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<void> {
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<void> {
await registerTaskProvider(
context,
new BlueprintTaskProvider("build", "build", "npx blueprint build", vscode.TaskGroup.Build),
)
registerTaskProvider(
await registerTaskProvider(
context,
new BlueprintTaskProvider(
"build-all",
Expand 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",
Expand 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",
Expand Down Expand Up @@ -208,14 +208,15 @@ export function registerBuildTasks(context: vscode.ExtensionContext): void {
)
}

function projectUsesBlueprint(): boolean {
async function projectUsesBlueprint(): Promise<boolean> {
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<string, unknown>
devDependencies?: Record<string, unknown>
}
Expand Down
17 changes: 7 additions & 10 deletions client/src/commands/saveBocDecompiledCommand.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -46,16 +45,14 @@ async function saveBoc(fileUri: vscode.Uri | undefined): Promise<void> {
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)
Expand Down
18 changes: 9 additions & 9 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<void> {
startServer(context).catch(consoleError)
registerBuildTasks(context)
await registerBuildTasks(context)
registerOpenBocCommand(context)
registerSaveBocDecompiledCommand(context)
registerMistiCommand(context)
Expand Down Expand Up @@ -817,14 +816,15 @@ function getInstallCommandForMisti(packageManager: PackageManager): string {
}
}

function projectUsesMisti(): boolean {
async function projectUsesMisti(): Promise<boolean> {
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<string, unknown>
devDependencies?: Record<string, unknown>
}
Expand All @@ -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(
Expand Down
32 changes: 14 additions & 18 deletions client/src/providers/BocDecompilerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.Uri> = new vscode.EventEmitter()
public readonly onDidChange: vscode.Event<vscode.Uri> = this._onDidChange.event

private readonly lastModified: Map<string, Date> = new Map()
private readonly lastModified: Map<vscode.Uri, Date> = 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<string> {
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<string> {
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,
Expand All @@ -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")
Expand All @@ -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)
}
}
3 changes: 1 addition & 2 deletions client/src/providers/BocFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.FileChangeEvent[]> =
Expand Down Expand Up @@ -31,7 +30,7 @@ export class BocFileSystemProvider implements vscode.FileSystemProvider {
public async readFile(uri: vscode.Uri): Promise<Uint8Array> {
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({
Expand Down
29 changes: 19 additions & 10 deletions client/src/utils/package-manager.ts
Original file line number Diff line number Diff line change
@@ -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<PackageManager> {
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
}

Expand All @@ -49,3 +49,12 @@ export function detectPackageManager(): PackageManager {

return "npm"
}

async function pathExits(path: vscode.Uri, file: string): Promise<boolean> {
try {
await vscode.workspace.fs.stat(vscode.Uri.joinPath(path, file))
return true
} catch {
return false
}
}
Loading
Loading