From babe240a9f68a0416c28a56ac8147a3fd557d667 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:52:37 +0400 Subject: [PATCH 01/16] initial implementation --- .../code/src/common/call-stack-parser.test.ts | 184 ++++++++++++++++++ editors/code/src/common/call-stack-parser.ts | 120 ++++++++++++ .../code/src/common/types/raw-transaction.ts | 3 + editors/code/src/common/types/transaction.ts | 1 + editors/code/src/extension.ts | 155 ++++++++++++++- .../src/providers/sandbox/TestTreeProvider.ts | 169 ++++++++++++++++ .../providers/sandbox/TestWebviewProvider.ts | 127 ++++++++++++ .../src/providers/sandbox/WebSocketServer.ts | 78 ++++++++ .../code/src/providers/sandbox/test-types.ts | 75 +++++++ .../views/actions/sandbox-actions-types.ts | 8 + .../TransactionTree.module.css | 29 +++ .../TransactionTree/TransactionTree.tsx | 40 +++- .../src/views/tests/TestsApp.module.css | 116 +++++++++++ .../webview-ui/src/views/tests/TestsApp.tsx | 67 +++++++ .../src/views/tests/components/TestsView.tsx | 161 +++++++++++++++ .../src/views/tests/hooks/useTestsApp.tsx | 64 ++++++ .../src/views/tests/sandbox-tests-types.ts | 36 ++++ .../webview-ui/src/views/tests/tests-main.tsx | 23 +++ package.json | 38 +++- server/src/server.ts | 52 ++++- shared/src/shared-msgtypes.ts | 11 ++ webpack.config.js | 1 + yarn.lock | 122 ++++++++++++ 23 files changed, 1673 insertions(+), 7 deletions(-) create mode 100644 editors/code/src/common/call-stack-parser.test.ts create mode 100644 editors/code/src/common/call-stack-parser.ts create mode 100644 editors/code/src/providers/sandbox/TestTreeProvider.ts create mode 100644 editors/code/src/providers/sandbox/TestWebviewProvider.ts create mode 100644 editors/code/src/providers/sandbox/WebSocketServer.ts create mode 100644 editors/code/src/providers/sandbox/test-types.ts create mode 100644 editors/code/src/webview-ui/src/views/tests/TestsApp.module.css create mode 100644 editors/code/src/webview-ui/src/views/tests/TestsApp.tsx create mode 100644 editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx create mode 100644 editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx create mode 100644 editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts create mode 100644 editors/code/src/webview-ui/src/views/tests/tests-main.tsx diff --git a/editors/code/src/common/call-stack-parser.test.ts b/editors/code/src/common/call-stack-parser.test.ts new file mode 100644 index 00000000..f04bd9b8 --- /dev/null +++ b/editors/code/src/common/call-stack-parser.test.ts @@ -0,0 +1,184 @@ +import {parseCallStack, CallStackEntry} from "./call-stack-parser" + +describe("Parse Call Stack", () => { + it("should parse empty call stack", () => { + expect(parseCallStack(undefined)).toEqual([]) + expect(parseCallStack("")).toEqual([]) + expect(parseCallStack("no stack traces")).toEqual([]) + }) + + it("should parse call stack from user example", () => { + const callStack = `Error: + at /root/node_modules/ton-sandbox-server-dev/dist/blockchain/Blockchain.js:294:24 + at AsyncLock.with (/root/node_modules/ton-sandbox-server-dev/dist/utils/AsyncLock.js:40:26) + at processTicksAndRejections (node:internal/process/task_queues:105:5) + at Blockchain.pushMessage (/root/node_modules/ton-sandbox-server-dev/dist/blockchain/Blockchain.js:291:9) + at BlockchainContractProvider.external (/root/node_modules/ton-sandbox-server-dev/dist/blockchain/BlockchainContractProvider.js:94:9) + at Object.send (/root/node_modules/ton-sandbox-server-dev/dist/treasury/Treasury.js:68:17) + at BlockchainContractProvider.internal (/root/node_modules/ton-sandbox-server-dev/dist/blockchain/BlockchainContractProvider.js:112:9) + at JettonWallet.sendTransfer (/root/wrappers/01_jetton/JettonWallet.ts:58:9) + at Proxy. (/root/node_modules/ton-sandbox-server-dev/dist/blockchain/Blockchain.js:652:39) + at Object. (/root/tests/01_jetton/JettonWallet.spec.ts:637:47) (at console. (file:///Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js:201:30974))` + + const expected: CallStackEntry[] = [ + { + function: "", + file: "/root/node_modules/ton-sandbox-server-dev/dist/blockchain/Blockchain.js", + line: 294, + column: 24, + }, + { + function: "AsyncLock.with", + file: "/root/node_modules/ton-sandbox-server-dev/dist/utils/AsyncLock.js", + line: 40, + column: 26, + }, + { + function: "processTicksAndRejections", + file: "node:internal/process/task_queues", + line: 105, + column: 5, + }, + { + function: "Blockchain.pushMessage", + file: "/root/node_modules/ton-sandbox-server-dev/dist/blockchain/Blockchain.js", + line: 291, + column: 9, + }, + { + function: "BlockchainContractProvider.external", + file: "/root/node_modules/ton-sandbox-server-dev/dist/blockchain/BlockchainContractProvider.js", + line: 94, + column: 9, + }, + { + function: "Object.send", + file: "/root/node_modules/ton-sandbox-server-dev/dist/treasury/Treasury.js", + line: 68, + column: 17, + }, + { + function: "BlockchainContractProvider.internal", + file: "/root/node_modules/ton-sandbox-server-dev/dist/blockchain/BlockchainContractProvider.js", + line: 112, + column: 9, + }, + { + function: "JettonWallet.sendTransfer", + file: "/root/wrappers/01_jetton/JettonWallet.ts", + line: 58, + column: 9, + }, + { + function: "Proxy.", + file: "/root/node_modules/ton-sandbox-server-dev/dist/blockchain/Blockchain.js", + line: 652, + column: 39, + }, + { + function: "Object.", + file: "/root/tests/01_jetton/JettonWallet.spec.ts", + line: 637, + column: 47, + }, + ] + + const result = parseCallStack(callStack) + expect(result).toEqual(expected) + }) + + it("should parse simple function calls", () => { + const callStack = `Error: + at foo (bar.js:10:5) + at baz (qux.ts:20:15)` + + const expected: CallStackEntry[] = [ + { + function: "foo", + file: "bar.js", + line: 10, + column: 5, + }, + { + function: "baz", + file: "qux.ts", + line: 20, + column: 15, + }, + ] + + expect(parseCallStack(callStack)).toEqual(expected) + }) + + it("should parse calls without locations", () => { + const callStack = `Error: + at foo + at bar (baz.js:5:10) + at /some/path/file.js:42` + + const expected: CallStackEntry[] = [ + { + function: "foo", + }, + { + function: "bar", + file: "baz.js", + line: 5, + column: 10, + }, + { + function: "", + file: "/some/path/file.js", + line: 42, + }, + ] + + expect(parseCallStack(callStack)).toEqual(expected) + }) + + it("should parse calls with only file and line", () => { + const callStack = `Error: + at test.js:42 + at another.ts:100` + + const expected: CallStackEntry[] = [ + { + function: "", + file: "test.js", + line: 42, + }, + { + function: "", + file: "another.ts", + line: 100, + }, + ] + + expect(parseCallStack(callStack)).toEqual(expected) + }) + + it('should ignore lines that do not start with "at "', () => { + const callStack = `Error: Something went wrong + This is not a stack trace line + at foo (bar.js:1:2) + Another non-stack line + at baz (qux.ts:3:4)` + + const expected: CallStackEntry[] = [ + { + function: "foo", + file: "bar.js", + line: 1, + column: 2, + }, + { + function: "baz", + file: "qux.ts", + line: 3, + column: 4, + }, + ] + + expect(parseCallStack(callStack)).toEqual(expected) + }) +}) diff --git a/editors/code/src/common/call-stack-parser.ts b/editors/code/src/common/call-stack-parser.ts new file mode 100644 index 00000000..ceb74066 --- /dev/null +++ b/editors/code/src/common/call-stack-parser.ts @@ -0,0 +1,120 @@ +export interface CallStackEntry { + readonly function: string + readonly file?: string + readonly line?: number + readonly column?: number +} + +export function parseCallStack(callStack: string | undefined): CallStackEntry[] { + if (!callStack) { + return [] + } + + const lines = callStack.split("\n").filter(line => line.trim().startsWith("at ")) + const entries: CallStackEntry[] = [] + + for (const line of lines) { + const trimmed = line.trim() + const entry = parseStackLine(trimmed) + if (entry) { + entries.push(entry) + } + } + + return entries +} + +function parseStackLine(line: string): CallStackEntry | null { + const withoutAt = line.startsWith("at ") ? line.slice(3).trim() : line.trim() + + if (!withoutAt) { + return {function: ""} + } + + // Handle different formats: + // 1. function (file:line:column) + // 2. file:line:column + // 3. function + + const parenIndex = withoutAt.indexOf("(") + const lastParenIndex = withoutAt.lastIndexOf(")") + + if (parenIndex !== -1 && lastParenIndex !== -1 && lastParenIndex > parenIndex) { + // Format: function (file:line:column) [extra info] + const functionName = withoutAt.slice(0, parenIndex).trim() + const locationAndExtra = withoutAt.slice(parenIndex + 1, lastParenIndex).trim() + + const locationEnd = locationAndExtra.indexOf(" (") + const location = + locationEnd === -1 ? locationAndExtra : locationAndExtra.slice(0, locationEnd) + + const locationParts = parseLocation(location) + return { + function: functionName, + ...locationParts, + } + } else { + // Format: file:line:column or just function + // Check if it looks like a file path (contains / or \ or has extension or starts with known schemes) + const firstPart = withoutAt.split(":")[0] + if ( + withoutAt.includes("/") || + withoutAt.includes("\\") || + /\.\w+$/.test(firstPart) || + firstPart.includes("node:") || + firstPart.startsWith("file://") || + /^\w+:/.test(withoutAt) + ) { + const locationParts = parseLocation(withoutAt) + return { + function: "", + ...locationParts, + } + } else { + return { + function: withoutAt, + } + } + } +} + +function parseLocation(location: string): {file?: string; line?: number; column?: number} { + // Handle cases like: + // /path/to/file.js:123:45 + // node:internal/process/task_queues:105:5 + // C:\path\to\file.js:123:45 + + const parts = location.split(":") + if (parts.length >= 3) { + const columnStr = parts.at(-1) ?? "" + const lineStr = parts.at(-2) ?? "" + const column = Number.parseInt(columnStr, 10) + const line = Number.parseInt(lineStr, 10) + + if (!Number.isNaN(column) && !Number.isNaN(line)) { + const file = parts.slice(0, -2).join(":") + return { + file, + line, + column, + } + } + } + + if (parts.length >= 2) { + const lineStr = parts.at(-1) ?? "" + const line = Number.parseInt(lineStr, 10) + + if (!Number.isNaN(line)) { + const file = parts.slice(0, -1).join(":") + return { + file, + line, + } + } + } + + return { + file: location, + } +} diff --git a/editors/code/src/common/types/raw-transaction.ts b/editors/code/src/common/types/raw-transaction.ts index 10cf21c5..bf352760 100644 --- a/editors/code/src/common/types/raw-transaction.ts +++ b/editors/code/src/common/types/raw-transaction.ts @@ -43,6 +43,7 @@ export interface RawTransactionInfo { readonly childrenIds: string[] readonly oldStorage: HexString | undefined readonly newStorage: HexString | undefined + readonly callStack: string | undefined } // temp type only for building @@ -63,6 +64,7 @@ interface MutableTransactionInfo { readonly contractName: string | undefined readonly oldStorage: Cell | undefined readonly newStorage: Cell | undefined + readonly callStack: string | undefined parent: TransactionInfo | undefined children: TransactionInfo[] } @@ -141,6 +143,7 @@ const processRawTx = ( children: [], oldStorage: tx.oldStorage ? Cell.fromHex(tx.oldStorage) : undefined, newStorage: tx.newStorage ? Cell.fromHex(tx.newStorage) : undefined, + callStack: tx.callStack, } visited.set(tx, result) diff --git a/editors/code/src/common/types/transaction.ts b/editors/code/src/common/types/transaction.ts index bc21eefa..b83e99e8 100644 --- a/editors/code/src/common/types/transaction.ts +++ b/editors/code/src/common/types/transaction.ts @@ -45,6 +45,7 @@ export interface TransactionInfo { readonly children: readonly TransactionInfo[] readonly oldStorage: Cell | undefined readonly newStorage: Cell | undefined + readonly callStack: string | undefined } export type TransactionInfoData = InternalTransactionInfoData | ExternalTransactionInfoData diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 54fb042b..55524857 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -13,8 +13,11 @@ import { TransportKind, } from "vscode-languageclient/node" +import {Cell, loadStateInit} from "@ton/core" + import { DocumentationAtPositionRequest, + GetWorkspaceContractsAbiResponse, GetContractAbiParams, GetContractAbiResponse, SetToolchainVersionNotification, @@ -22,6 +25,7 @@ import { TypeAtPositionParams, TypeAtPositionRequest, TypeAtPositionResponse, + WorkspaceContractInfo, } from "@shared/shared-msgtypes" import type {ClientOptions} from "@shared/config-scheme" @@ -37,18 +41,36 @@ import {BocEditorProvider} from "./providers/boc/BocEditorProvider" import {BocFileSystemProvider} from "./providers/boc/BocFileSystemProvider" import {BocDecompilerProvider} from "./providers/boc/BocDecompilerProvider" import {registerSaveBocDecompiledCommand} from "./commands/saveBocDecompiledCommand" -import {registerSandboxCommands} from "./commands/sandboxCommands" +import {registerSandboxCommands, openFileAtPosition} from "./commands/sandboxCommands" +import {parseCallStack} from "./common/call-stack-parser" import {SandboxTreeProvider} from "./providers/sandbox/SandboxTreeProvider" import {SandboxActionsProvider} from "./providers/sandbox/SandboxActionsProvider" import {HistoryWebviewProvider} from "./providers/sandbox/HistoryWebviewProvider" import {TransactionDetailsProvider} from "./providers/sandbox/TransactionDetailsProvider" import {SandboxCodeLensProvider} from "./providers/sandbox/SandboxCodeLensProvider" +import {TestTreeProvider} from "./providers/sandbox/TestTreeProvider" +import {TestWebviewProvider} from "./providers/sandbox/TestWebviewProvider" +import {WebSocketServer} from "./providers/sandbox/WebSocketServer" import {configureDebugging} from "./debugging" +import {ContractData, TestRun} from "./providers/sandbox/test-types" +import {TransactionDetailsInfo} from "./common/types/transaction" +import {DeployedContract} from "./common/types/contract" +import {Base64String} from "./common/base64-string" let client: LanguageClient | undefined = undefined let cachedToolchainInfo: SetToolchainVersionParams | undefined = undefined +function sameNameContract(it: WorkspaceContractInfo, contract: ContractData | undefined): boolean { + const leftName = it.name + const rightName = contract?.meta?.wrapperName ?? "" + + const normalizedLeft = leftName.toLowerCase().replace("-contract", "").replace(/-/g, "") + const normalizedRight = rightName.toLowerCase().replace("-contract", "").replace(/-/g, "") + + return normalizedLeft === normalizedRight +} + export async function activate(context: vscode.ExtensionContext): Promise { await checkConflictingExtensions() @@ -90,6 +112,123 @@ export async function activate(context: vscode.ExtensionContext): Promise const transactionDetailsProvider = new TransactionDetailsProvider(context.extensionUri) + const testTreeProvider = new TestTreeProvider() + context.subscriptions.push( + vscode.window.createTreeView("tonTestResultsTree", { + treeDataProvider: testTreeProvider, + showCollapseAll: false, + }), + ) + + const testWebviewProvider = new TestWebviewProvider(context.extensionUri, testTreeProvider) + context.subscriptions.push( + vscode.window.registerWebviewViewProvider( + TestWebviewProvider.viewType, + testWebviewProvider, + ), + vscode.commands.registerCommand( + "ton.test.showTransactionDetails", + async (testRun: TestRun) => { + const workspaceContracts = + await vscode.commands.executeCommand( + "tolk.getWorkspaceContractsAbi", + ) + + console.log("workspaceContracts", workspaceContracts.contracts.length) + console.log( + "workspaceContracts", + workspaceContracts.contracts.filter(it => it.name.includes("jetton")), + ) + + console.log(`txs of ${testRun.id}`, testRun.transactions) + + // TODO: redone + const transaction = testRun.transactions.at(1) ?? testRun.transactions.at(0) + if (transaction) { + const contract = testRun.contracts.find( + it => it.address === transaction.address?.toString(), + ) + + const stateInit = loadStateInit( + Cell.fromHex(contract?.stateInit ?? "").asSlice(), + ) + + const workspaceContract = workspaceContracts.contracts.find(it => + sameNameContract(it, contract), + ) + const abi = workspaceContract?.abi + console.log("contract data", contract) + console.log("workspaceContract", workspaceContract) + console.log("abi", abi) + + const transactionDetailsInfo: TransactionDetailsInfo = { + contractAddress: transaction.address?.toString() ?? "unknown", + methodName: "test-transaction", + transactionId: transaction.transaction.lt.toString(), + timestamp: new Date().toISOString(), + status: "success" as const, // For now, assume success + resultString: testRun.resultString, + deployedContracts: testRun.contracts.map((contract): DeployedContract => { + const workspaceContract = workspaceContracts.contracts.find(it => + sameNameContract(it, contract), + ) + return { + abi: workspaceContract?.abi, + sourceMap: undefined, + address: contract.address, + deployTime: undefined, + name: contract.meta?.wrapperName ?? "unknown", + sourceUri: "", + } + }), + account: contract?.account, + stateInit: { + code: (stateInit.code ?? new Cell()) + .toBoc() + .toString("base64") as Base64String, + data: (stateInit.data ?? new Cell()) + .toBoc() + .toString("base64") as Base64String, + }, + abi, + } + + console.log("transactionDetailsInfo", transactionDetailsInfo) + transactionDetailsProvider.showTransactionDetails(transactionDetailsInfo) + } + }, + ), + vscode.commands.registerCommand( + "ton.test.openTestSource", + (treeItem: {command?: vscode.Command}) => { + const testRun = treeItem.command?.arguments?.[0] as TestRun | undefined + if (!testRun) { + console.error("No testRun found in tree item arguments") + return + } + + const transactionWithCallStack = testRun.transactions.find(tx => tx.callStack) + if (transactionWithCallStack?.callStack) { + const parsedCallStack = parseCallStack(transactionWithCallStack.callStack) + if (parsedCallStack.length > 0) { + const lastEntry = parsedCallStack.at(-1) + if ( + lastEntry?.file && + lastEntry.line !== undefined && + lastEntry.column !== undefined + ) { + openFileAtPosition( + lastEntry.file, + lastEntry.line - 1, + lastEntry.column - 1, + ) + } + } + } + }, + ), + ) + const sandboxCodeLensProvider = new SandboxCodeLensProvider(sandboxTreeProvider) context.subscriptions.push( vscode.languages.registerCodeLensProvider({language: "tolk"}, sandboxCodeLensProvider), @@ -98,7 +237,16 @@ export async function activate(context: vscode.ExtensionContext): Promise sandboxTreeProvider.setActionsProvider(sandboxActionsProvider) sandboxTreeProvider.setCodeLensProvider(sandboxCodeLensProvider) + const wsServer = new WebSocketServer() + wsServer.start() + wsServer.setTestWebviewProvider(testWebviewProvider) + context.subscriptions.push( + { + dispose: () => { + wsServer.dispose() + }, + }, vscode.window.onDidChangeActiveTextEditor(editor => { if (editor) { const document = { @@ -128,6 +276,11 @@ export async function activate(context: vscode.ExtensionContext): Promise return client?.sendRequest("tolk.getContractAbi", params) }, ), + vscode.commands.registerCommand("tolk.getWorkspaceContractsAbi", async () => { + return client?.sendRequest( + "tolk.getWorkspaceContractsAbi", + ) + }), ...sandboxCommands, ) diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts new file mode 100644 index 00000000..46e7eb71 --- /dev/null +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core +import * as vscode from "vscode" + +import {processTxString, TestDataMessage, TestRun} from "./test-types" + +interface TestTreeItem { + readonly id: string + readonly label: string + readonly description?: string + readonly contextValue?: string + readonly iconPath?: vscode.ThemeIcon + readonly collapsibleState?: vscode.TreeItemCollapsibleState + readonly command?: vscode.Command + readonly type: "testName" | "testRun" | "message" +} + +export class TestTreeProvider implements vscode.TreeDataProvider { + private readonly _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter() + public readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event + + private readonly testRunsByName: Map = new Map() + + public addTestData(data: TestDataMessage): void { + const transactions = processTxString(data.transactions) + + console.log(data, "with", transactions.length, "transactions") + + const testRun: TestRun = { + id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, + name: data.testName, + timestamp: Date.now(), + transactions: transactions, + contracts: data.contracts, + changes: data.changes, + resultString: data.transactions, + } + + const existingRuns = this.testRunsByName.get(data.testName) ?? [] + existingRuns.push(testRun) + + this.testRunsByName.set(data.testName, existingRuns) + + this._onDidChangeTreeData.fire(undefined) + } + + public getTreeItem(element: TestTreeItem): vscode.TreeItem { + return { + id: element.id, + label: element.label, + description: element.description, + contextValue: element.contextValue, + iconPath: element.iconPath, + collapsibleState: element.collapsibleState, + command: element.command, + } + } + + public getChildren(element?: TestTreeItem): Thenable { + if (!element) { + // Корень дерева: возвращаем уникальные имена тестов + return Promise.resolve( + [...this.testRunsByName.keys()].map(testName => ({ + id: `test-group-${testName}`, + label: testName, + description: `${this.testRunsByName.get(testName)?.length ?? 0} runs`, + contextValue: "testGroup", + iconPath: new vscode.ThemeIcon("beaker"), + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + type: "testName" as const, + })), + ) + } + + if (element.type === "testName") { + // Для имени теста возвращаем test run как "Transaction #1", "#2", etc. + const testRuns = this.testRunsByName.get(element.label) ?? [] + console.log("testRuns", testRuns.length) + return Promise.resolve( + testRuns.map((testRun, index) => ({ + id: `${element.id}-run-${index}`, + label: `Transaction #${index}`, + description: new Date(testRun.timestamp).toLocaleTimeString(), + contextValue: "testRun", + iconPath: new vscode.ThemeIcon("check"), + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + type: "testRun" as const, + command: { + command: "ton.test.showTransactionDetails", + title: "Show Test Run Details", + arguments: [testRun], + }, + })), + ) + } + + if (element.type === "testRun") { + // Для test run возвращаем сообщения внутри транзакции + const testName = element.id.split("-run-")[0].replace("test-group-", "") + const runIndexStr = element.id.split("-run-")[1] + const runIndex = Number.parseInt(runIndexStr) + const testRuns = this.testRunsByName.get(testName) ?? [] + const testRun = testRuns.at(runIndex) // Обратный порядок из-за unshift + + const messageItems: TestTreeItem[] = [] + + // Добавляем входящее сообщение, если оно есть + if (testRun) { + testRun.transactions.forEach((tx, txIndex) => { + if (tx.transaction.inMessage) { + messageItems.push({ + id: `${element.id}-tx-${txIndex}-in`, + label: `Incoming Message`, + description: `To: ${tx.address?.toString().slice(0, 10) ?? "unknown"}...`, + contextValue: "message", + iconPath: new vscode.ThemeIcon("arrow-right"), + collapsibleState: vscode.TreeItemCollapsibleState.None, + type: "message" as const, + }) + } + + // Добавляем исходящие действия + tx.outActions.forEach((action, actionIndex) => { + messageItems.push({ + id: `${element.id}-tx-${txIndex}-out-${actionIndex}`, + label: `Outgoing Action: ${action.type}`, + description: action.type === "sendMsg" ? "Send Message" : action.type, + contextValue: "message", + iconPath: new vscode.ThemeIcon("arrow-left"), + collapsibleState: vscode.TreeItemCollapsibleState.None, + type: "message" as const, + }) + }) + }) + } + + return Promise.resolve(messageItems) + } + + return Promise.resolve([]) + } + + public getTestRun(testRunId: string): TestRun | undefined { + for (const testRuns of this.testRunsByName.values()) { + const found = testRuns.find(run => run.id === testRunId) + if (found) return found + } + return undefined + } + + public clearAllTests(): void { + this.testRunsByName.clear() + this._onDidChangeTreeData.fire(undefined) + } + + public removeTestRun(testRunId: string): void { + for (const [testName, testRuns] of this.testRunsByName.entries()) { + const filteredRuns = testRuns.filter(run => run.id !== testRunId) + if (filteredRuns.length === 0) { + this.testRunsByName.delete(testName) + } else { + this.testRunsByName.set(testName, filteredRuns) + } + } + this._onDidChangeTreeData.fire(undefined) + } +} diff --git a/editors/code/src/providers/sandbox/TestWebviewProvider.ts b/editors/code/src/providers/sandbox/TestWebviewProvider.ts new file mode 100644 index 00000000..f49b4262 --- /dev/null +++ b/editors/code/src/providers/sandbox/TestWebviewProvider.ts @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core +import * as vscode from "vscode" + +import {TestTreeProvider} from "./TestTreeProvider" +import {TestDataMessage} from "./test-types" + +interface TestCommand { + readonly type: "clearAllTests" | "removeTest" | "showTransactionDetails" + readonly testId?: string + readonly testRunId?: string + readonly transactionId?: string +} + +interface TestMessage { + readonly type: "addTestData" + readonly data: TestDataMessage +} + +export class TestWebviewProvider implements vscode.WebviewViewProvider { + public static readonly viewType: string = "tonTestResults" + + private view?: vscode.WebviewView + + public constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _treeProvider: TestTreeProvider, + ) {} + + public resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ): void { + this.view = webviewView + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri], + } + + webviewView.webview.html = this.getHtmlForWebview(webviewView.webview) + + webviewView.webview.onDidReceiveMessage((command: TestCommand) => { + switch (command.type) { + case "clearAllTests": { + this._treeProvider.clearAllTests() + break + } + case "removeTest": { + if (command.testId) { + this._treeProvider.removeTestRun(command.testId) + } + break + } + case "showTransactionDetails": { + if (command.testRunId && command.transactionId) { + const testRun = this._treeProvider.getTestRun(command.testRunId) + if (testRun) { + // const transaction = testRun.transactions.find( + // tx => tx.id === command.transactionId, + // ) + // if (transaction) { + // // Здесь можно открыть детали транзакции + // vscode.window.showInformationMessage( + // `Transaction Details: ${transaction.address} (LT: ${transaction.lt})`, + // ) + // } + } + } + break + } + default: { + console.warn("Unknown command type:", command.type) + } + } + }) + } + + public addTestData(data: TestDataMessage): void { + this._treeProvider.addTestData(data) + + // Отправляем данные в webview + if (this.view) { + const message: TestMessage = { + type: "addTestData", + data, + } + void this.view.webview.postMessage(message) + } + } + + private getHtmlForWebview(webview: vscode.Webview): string { + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionUri, "dist", "webview-ui", "tests.js"), + ) + const styleUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionUri, "dist", "webview-ui", "tests.css"), + ) + + const nonce = getNonce() + + return ` + + + + + + + Test Results + + +
+ + + ` + } +} + +function getNonce(): string { + let text = "" + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text +} diff --git a/editors/code/src/providers/sandbox/WebSocketServer.ts b/editors/code/src/providers/sandbox/WebSocketServer.ts new file mode 100644 index 00000000..b2b38b4c --- /dev/null +++ b/editors/code/src/providers/sandbox/WebSocketServer.ts @@ -0,0 +1,78 @@ +import * as vscode from "vscode" +import {WebSocket, Server} from "ws" + +import {TestDataMessage} from "./test-types" + +export class WebSocketServer { + private wss: Server | null = null + private disposables: vscode.Disposable[] = [] + private testWebviewProvider?: {addTestData: (data: TestDataMessage) => void} + + public constructor( + private readonly port: number = Number.parseInt( + process.env.VSCODE_WEBSOCKET_PORT ?? "7743", + 10, + ), + ) {} + + public setTestWebviewProvider(provider: {addTestData: (data: TestDataMessage) => void}): void { + this.testWebviewProvider = provider + } + + public start(): void { + try { + this.wss = new Server({port: this.port}) + + this.wss.on("connection", (ws: WebSocket) => { + console.log("Blockchain WebSocket connected") + + ws.on("message", (data: Buffer) => { + try { + const message = JSON.parse(data.toString()) as TestDataMessage + + this.handleTestData(message) + } catch (error) { + console.error("Failed to parse WebSocket message:", error) + } + }) + + ws.on("close", () => { + console.log("Blockchain WebSocket disconnected") + }) + + ws.on("error", error => { + console.error("WebSocket error:", error) + }) + }) + + this.wss.on("error", error => { + console.error("WebSocket Server error:", error) + }) + + console.log(`WebSocket server started on port ${this.port}`) + } catch (error) { + console.error("Failed to start WebSocket server:", error) + } + } + + private handleTestData(message: TestDataMessage): void { + this.testWebviewProvider?.addTestData(message) + } + + public stop(): void { + if (this.wss) { + this.wss.close() + this.wss = null + console.log("WebSocket server stopped") + } + + this.disposables.forEach(d => { + d.dispose() + }) + this.disposables = [] + } + + public dispose(): void { + this.stop() + } +} diff --git a/editors/code/src/providers/sandbox/test-types.ts b/editors/code/src/providers/sandbox/test-types.ts new file mode 100644 index 00000000..aa34222a --- /dev/null +++ b/editors/code/src/providers/sandbox/test-types.ts @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core + +import {Cell, ContractABI as BadContractABI, loadTransaction} from "@ton/core" + +import {TransactionInfo} from "../../common/types/transaction" +import { + processRawTransactions, + RawTransactionInfo, + RawTransactions, +} from "../../common/types/raw-transaction" +import {HexString} from "../../common/hex-string" + +export interface ContractMeta { + readonly wrapperName?: string + readonly abi?: BadContractABI | null + readonly treasurySeed?: string +} + +export interface ContractData { + readonly address: string + readonly stateInit?: HexString + readonly account?: HexString + readonly meta?: ContractMeta +} + +export interface StateChange { + readonly address: string + readonly lt: string + readonly before: string + readonly after: string +} + +export interface TestDataMessage { + readonly $: "test-data" + readonly testName: string + readonly transactions: string + readonly contracts: readonly ContractData[] + readonly changes: readonly StateChange[] +} + +export interface TestRun { + readonly id: string + readonly name: string + readonly timestamp: number + readonly resultString: string + readonly transactions: readonly TransactionInfo[] + readonly contracts: readonly ContractData[] + readonly changes: readonly StateChange[] +} + +function parseTransactions(data: string): RawTransactions | undefined { + try { + return JSON.parse(data) as RawTransactions + } catch { + return undefined + } +} + +export function processTxString(resultString: string): TransactionInfo[] { + const rawTxs = parseTransactions(resultString) + if (!rawTxs) { + return [] + } + + const parsedTransactions = rawTxs.transactions.map( + (it): RawTransactionInfo => ({ + ...it, + transaction: it.transaction, + parsedTransaction: loadTransaction(Cell.fromHex(it.transaction).asSlice()), + }), + ) + + return processRawTransactions(parsedTransactions) +} diff --git a/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts b/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts index 22a03a20..aed6c347 100644 --- a/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts +++ b/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts @@ -242,6 +242,13 @@ export interface UpdateConnectionStatusMessage { readonly isConnected: boolean } +export interface OpenFileAtPositionMessage { + readonly type: "openFileAtPosition" + readonly uri: string + readonly row: number + readonly column: number +} + export type VSCodeMessage = | UpdateContractsMessage | ShowResultMessage @@ -257,6 +264,7 @@ export type VSCodeMessage = | TemplateUpdatedMessage | TemplateDeletedMessage | UpdateConnectionStatusMessage + | OpenFileAtPositionMessage export type VSCodeCommand = | SendExternalMessageCommand diff --git a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css index a9d6c946..a16c2cfc 100644 --- a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css +++ b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css @@ -117,6 +117,7 @@ display: flex; flex-direction: column; gap: var(--spacing-md); + position: relative; } .tooltipField { @@ -170,6 +171,34 @@ gap: 2px; } +.nodeHeader { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.callStackButton { + background: none; + border: none; + cursor: pointer; + padding: 2px; + color: var(--color-text-tertiary); + opacity: 0.7; + transition: opacity 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--border-radius-sm); + position: absolute; + top: -2px; + right: -2px; +} + +.callStackButton:hover { + opacity: 1; + background-color: var(--color-background-hover); +} + .bottonText { display: flex; flex-direction: column; diff --git a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx index 37204fa2..1122cf4f 100644 --- a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx +++ b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx @@ -2,18 +2,22 @@ import React, {useMemo, useState, useRef, useEffect} from "react" import {Orientation, RawNodeDatum, TreeLinkDatum, Tree} from "react-d3-tree" import {Address} from "@ton/core" +import {FiExternalLink} from "react-icons/fi" import {TransactionDetails} from "../index" import {formatCurrency} from "../../../../components/format/format" import {ContractData} from "../../../../../../common/types/contract" import {TransactionInfo} from "../../../../../../common/types/transaction" -import {VSCodeTransactionDetailsAPI} from "../../transaction-details-types" import {ParsedDataView} from "../ParsedDataView/ParsedDataView" import {parseData, ParsedObject} from "../../../../../../common/binary" +import {CallStackEntry, parseCallStack} from "../../../../../../common/call-stack-parser" + +import {VSCodeTransactionDetailsAPI} from "../../transaction-details-types" + import {useTooltip} from "./useTooltip" import {SmartTooltip} from "./SmartTooltip" @@ -32,6 +36,7 @@ interface TransactionTooltipData { readonly totalFees: bigint } readonly sentTotal: bigint + readonly callStackPosition?: CallStackEntry } interface TransactionTreeProps { @@ -69,7 +74,13 @@ const formatAddressShort = (address: Address | undefined): string => { return addressStr.slice(0, 6) + "..." + addressStr.slice(-6) } -function TransactionTooltipContent({data}: {data: TransactionTooltipData}): React.JSX.Element { +function TransactionTooltipContent({ + data, + vscode, +}: { + data: TransactionTooltipData + vscode: VSCodeTransactionDetailsAPI +}): React.JSX.Element { return (
@@ -114,6 +125,23 @@ function TransactionTooltipContent({data}: {data: TransactionTooltipData}): Reac )}
+ + {data.callStackPosition && ( + + )} ) } @@ -262,17 +290,20 @@ export function TransactionTree({ const srcAddress = tx.transaction.inMessage?.info.src const fromAddressStr = srcAddress ? formatAddressShort(srcAddress as Address) : "unknown" + const parsedCallStack = parseCallStack(tx.callStack) + const tooltipData: TransactionTooltipData = { fromAddress: fromAddressStr, computePhase, fees, sentTotal: tx.money.sentTotal, + callStackPosition: parsedCallStack.length > 0 ? parsedCallStack.at(-1) : undefined, } showTooltip({ x: rect.left, y: rect.top, - content: , + content: , }) } @@ -357,6 +388,9 @@ export function TransactionTree({ const lt = tx.transaction.lt.toString() const isSelected = selectedTransaction?.transaction.lt.toString() === lt + const parsedCallStack = parseCallStack(tx.callStack) + const firstEntry = parsedCallStack.length > 0 ? parsedCallStack[0] : undefined + const hasExternalOut = tx.transaction.outMessages.values().some(outMsg => { return outMsg.info.type === "external-out" }) diff --git a/editors/code/src/webview-ui/src/views/tests/TestsApp.module.css b/editors/code/src/webview-ui/src/views/tests/TestsApp.module.css new file mode 100644 index 00000000..67dd9af1 --- /dev/null +++ b/editors/code/src/webview-ui/src/views/tests/TestsApp.module.css @@ -0,0 +1,116 @@ +.container { + height: 100vh; + display: flex; + flex-direction: column; +} + +.header { + padding: 12px 16px; + border-bottom: 1px solid var(--vscode-panel-border); + background-color: var(--vscode-editor-background); +} + +.header h2 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--vscode-foreground); +} + +.content { + flex: 1; + overflow: auto; +} + +.testItem { + display: flex; + align-items: center; + padding: 8px 16px; + border-bottom: 1px solid var(--vscode-list-inactiveSelectionBackground); + cursor: pointer; +} + +.testItem:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.testIcon { + margin-right: 8px; + color: var(--vscode-foreground); +} + +.testInfo { + flex: 1; +} + +.testName { + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); +} + +.testTime { + font-size: 11px; + color: var(--vscode-descriptionForeground); +} + +.transactionItem { + display: flex; + align-items: center; + padding: 6px 16px 6px 32px; + border-bottom: 1px solid var(--vscode-list-inactiveSelectionBackground); + cursor: pointer; +} + +.transactionItem:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.transactionIcon { + margin-right: 8px; +} + +.transactionInfo { + flex: 1; +} + +.transactionAddress { + font-size: 12px; + font-family: var(--vscode-editor-font-family); + color: var(--vscode-foreground); +} + +.transactionLt { + font-size: 10px; + color: var(--vscode-descriptionForeground); +} + +.toolbar { + display: flex; + gap: 8px; + padding: 8px 16px; + border-bottom: 1px solid var(--vscode-panel-border); +} + +.button { + padding: 4px 8px; + font-size: 11px; + border: 1px solid var(--vscode-button-border); + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-radius: 3px; + cursor: pointer; +} + +.button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.button.secondary { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); +} + +.button.secondary:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} diff --git a/editors/code/src/webview-ui/src/views/tests/TestsApp.tsx b/editors/code/src/webview-ui/src/views/tests/TestsApp.tsx new file mode 100644 index 00000000..2e2ab22d --- /dev/null +++ b/editors/code/src/webview-ui/src/views/tests/TestsApp.tsx @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core +import React, {useCallback, useEffect, useState} from "react" + +import {TestDataMessage} from "../../../../providers/sandbox/test-types" + +import {TestsView} from "./components/TestsView" +import {TestsAppProvider} from "./hooks/useTestsApp" +import {TestsVSCodeAPI, TestsCommand} from "./sandbox-tests-types" + +import "./TestsApp.module.css" + +interface TestsMessage { + readonly type: "addTestData" + readonly data: TestDataMessage +} + +interface TestsAppProps { + readonly vscode: TestsVSCodeAPI +} + +export function TestsApp({vscode}: TestsAppProps): React.JSX.Element { + const [messages, setMessages] = useState([]) + + const postMessage = useCallback( + (message: TestsCommand) => { + vscode.postMessage(message) + }, + [vscode], + ) + + useEffect(() => { + const handler = (event: MessageEvent): void => { + const message = event.data as TestsMessage + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (message.type === "addTestData") { + setMessages(prev => [...prev, message]) + } + } + + window.addEventListener("message", handler) + return () => { + window.removeEventListener("message", handler) + } + }, []) + + const handleClearAllTests = useCallback(() => { + postMessage({type: "clearAllTests"}) + }, [postMessage]) + + const handleRemoveTest = useCallback( + (testId: string) => { + postMessage({type: "removeTest", testId}) + }, + [postMessage], + ) + + return ( + + + + ) +} diff --git a/editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx b/editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx new file mode 100644 index 00000000..50d1ddc7 --- /dev/null +++ b/editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core +import React, {useCallback, useEffect, useState} from "react" + +import {useTestsApp} from "../hooks/useTestsApp" + +import styles from "../TestsApp.module.css" +import {TestDataMessage} from "../../../../../providers/sandbox/test-types" + +interface TestsViewProps { + readonly messages: readonly {type: string; data?: TestDataMessage}[] + readonly onClearAllTests: () => void + readonly onRemoveTest: (testId: string) => void +} + +export function TestsView({ + messages, + onClearAllTests, + onRemoveTest, +}: TestsViewProps): React.JSX.Element { + const {testRuns, addTestData} = useTestsApp() + const [expandedTests, setExpandedTests] = useState>(new Set()) + + // Обработка входящих сообщений + useEffect(() => { + messages.forEach(message => { + if (message.type === "addTestData" && message.data) { + addTestData(message.data) + } + }) + }, [messages, addTestData]) + + const toggleTestExpansion = useCallback((testId: string) => { + setExpandedTests(prev => { + const newSet = new Set(prev) + if (newSet.has(testId)) { + newSet.delete(testId) + } else { + newSet.add(testId) + } + return newSet + }) + }, []) + + const handleTransactionClick = useCallback((testRunId: string, transactionId: string) => { + // Отправляем сообщение в extension для открытия деталей транзакции + // vscode API будет передан через контекст или пропс в будущем + console.log("Show transaction details:", testRunId, transactionId) + }, []) + + return ( +
+
+

Test Results

+
+ +
+ + + {testRuns.length} test run{testRuns.length === 1 ? "" : "s"} + +
+ +
+ {testRuns.length === 0 ? ( +
+ No test runs yet. Run your tests to see results here. +
+ ) : ( + testRuns.map(testRun => ( +
+
{ + toggleTestExpansion(testRun.id) + }} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") { + toggleTestExpansion(testRun.id) + } + }} + > + +
+
{testRun.name}
+
+ {new Date(testRun.timestamp).toLocaleString()} +
+
+ +
+ + {expandedTests.has(testRun.id) && ( +
+ {testRun.transactions.map(transaction => ( +
{ + handleTransactionClick(testRun.id, transaction.transaction.lt.toString()) + }} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") { + handleTransactionClick(testRun.id, transaction.transaction.lt.toString()) + } + }} + > + +
+
+ {transaction.address?.toString()} +
+
+ LT: {transaction.transaction.lt.toString()} +
+
+
+ ))} +
+ )} +
+ )) + )} +
+
+ ) +} diff --git a/editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx b/editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx new file mode 100644 index 00000000..f9448a42 --- /dev/null +++ b/editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core +import React, {createContext, ReactNode, useCallback, useContext, useMemo, useState} from "react" + +import { + processTxString, + TestDataMessage, + TestRun, +} from "../../../../../providers/sandbox/test-types" + +interface TestsAppContextValue { + readonly testRuns: readonly TestRun[] + readonly addTestData: (data: TestDataMessage) => void + readonly clearAllTests: () => void + readonly removeTest: (testId: string) => void +} + +const TestsAppContext = createContext(null) + +export function TestsAppProvider({children}: {children: ReactNode}): React.JSX.Element { + const [testRuns, setTestRuns] = useState([]) + + const addTestData = useCallback((data: TestDataMessage) => { + const testRun: TestRun = { + id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, + name: data.testName, + timestamp: Date.now(), + transactions: processTxString(data.transactions), + contracts: data.contracts, + changes: data.changes, + resultString: data.transactions, + } + + setTestRuns(prev => [testRun, ...prev.slice(0, 49)]) // Ограничиваем до 50 тестов + }, []) + + const clearAllTests = useCallback(() => { + setTestRuns([]) + }, []) + + const removeTest = useCallback((testId: string) => { + setTestRuns(prev => prev.filter(run => run.id !== testId)) + }, []) + + const value = useMemo( + () => ({ + testRuns, + addTestData, + clearAllTests, + removeTest, + }), + [testRuns, addTestData, clearAllTests, removeTest], + ) + + return {children} +} + +export function useTestsApp(): TestsAppContextValue { + const context = useContext(TestsAppContext) + if (!context) { + throw new Error("useTestsApp must be used within TestsAppProvider") + } + return context +} diff --git a/editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts b/editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts new file mode 100644 index 00000000..5c20bc1d --- /dev/null +++ b/editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core + +import {TestDataMessage} from "../../../../providers/sandbox/test-types" + +export interface TestsVSCodeAPI { + readonly postMessage: (command: TestsCommand) => void + readonly getState: () => unknown + readonly setState: (state: unknown) => void +} + +export interface AddTestDataCommand { + readonly type: "addTestData" + readonly data: TestDataMessage +} + +export interface ClearAllTestsCommand { + readonly type: "clearAllTests" +} + +export interface RemoveTestCommand { + readonly type: "removeTest" + readonly testId: string +} + +export interface ShowTransactionDetailsCommand { + readonly type: "showTransactionDetails" + readonly testRunId: string + readonly transactionId: string +} + +export type TestsCommand = + | AddTestDataCommand + | ClearAllTestsCommand + | RemoveTestCommand + | ShowTransactionDetailsCommand diff --git a/editors/code/src/webview-ui/src/views/tests/tests-main.tsx b/editors/code/src/webview-ui/src/views/tests/tests-main.tsx new file mode 100644 index 00000000..004e3a43 --- /dev/null +++ b/editors/code/src/webview-ui/src/views/tests/tests-main.tsx @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2025 TON Core +import React from "react" +import {createRoot} from "react-dom/client" + +import {TestsApp} from "./TestsApp" +import {TestsVSCodeAPI} from "./sandbox-tests-types" + +declare function acquireVsCodeApi(): TestsVSCodeAPI + +const vscode = acquireVsCodeApi() + +const container = document.querySelector("#root") +if (!container) { + throw new Error("Root element not found") +} + +const root = createRoot(container) +root.render( + + + , +) diff --git a/package.json b/package.json index 7d478445..0ab7924f 100644 --- a/package.json +++ b/package.json @@ -247,6 +247,11 @@ "title": "Get Contract ABI", "category": "Tolk" }, + { + "command": "tolk.getWorkspaceContractsAbi", + "title": "Get Workspace Contracts ABI", + "category": "Tolk" + }, { "command": "tolk.executeHoverProvider", "title": "Get Documentation At Position", @@ -354,6 +359,12 @@ "category": "TON", "icon": "$(copy)" }, + { + "command": "ton.test.openTestSource", + "title": "Open Test Source", + "category": "TON", + "icon": "$(go-to-file)" + }, { "command": "ton.sandbox.installServer", "title": "Install TON Sandbox Server", @@ -398,6 +409,19 @@ "name": "History", "when": "true" } + ], + "tonTestContainer": [ + { + "id": "tonTestResultsTree", + "name": "Tests Tree", + "when": "true" + }, + { + "id": "tonTestResults", + "type": "webview", + "name": "Test Results", + "when": "true" + } ] }, "viewsContainers": { @@ -406,6 +430,11 @@ "id": "tonSandboxContainer", "title": "TON Sandbox", "icon": "$(rocket)" + }, + { + "id": "tonTestContainer", + "title": "TON Tests", + "icon": "$(beaker)" } ] }, @@ -497,6 +526,11 @@ "command": "ton.sandbox.copyContractAddressFromTree", "when": "view == tonSandbox && viewItem == deployed-contract", "group": "inline" + }, + { + "command": "ton.test.openTestSource", + "when": "view == tonTestResultsTree && viewItem == testRun", + "group": "inline" } ] }, @@ -785,7 +819,8 @@ "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", "vscode-uri": "^3.0.7", - "web-tree-sitter": "^0.25.0" + "web-tree-sitter": "^0.25.0", + "ws": "^8.18.3" }, "devDependencies": { "@babel/core": "^7.26.0", @@ -798,6 +833,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/vscode": "^1.63.0", + "@types/ws": "^8", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^3.6.0", diff --git a/server/src/server.ts b/server/src/server.ts index f938b2ac..dc72b507 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -5,7 +5,6 @@ import * as path from "node:path" import {fileURLToPath} from "node:url" import * as lsp from "vscode-languageserver" - import {DidChangeWatchedFilesParams, FileChangeType, RenameFilesParams} from "vscode-languageserver" import {WorkspaceEdit} from "vscode-languageserver-types" @@ -15,7 +14,11 @@ import type {Node as SyntaxNode} from "web-tree-sitter" import {TextDocument} from "vscode-languageserver-textdocument" import {asParserPoint} from "@server/utils/position" -import {index as tolkIndex, IndexRoot as TolkIndexRoot} from "@server/languages/tolk/indexes" +import { + index as tolkIndex, + IndexKey, + IndexRoot as TolkIndexRoot, +} from "@server/languages/tolk/indexes" import {index as funcIndex, IndexRoot as FuncIndexRoot} from "@server/languages/func/indexes" import {globalVFS} from "@server/vfs/global" @@ -24,13 +27,16 @@ import type {ClientOptions} from "@shared/config-scheme" import { ContractAbiRequest, DocumentationAtPositionRequest, + GetAllContractsAbiRequest, GetContractAbiParams, GetContractAbiResponse, + GetWorkspaceContractsAbiResponse, SetToolchainVersionNotification, SetToolchainVersionParams, TypeAtPositionParams, TypeAtPositionRequest, TypeAtPositionResponse, + WorkspaceContractInfo, } from "@shared/shared-msgtypes" import {Logger} from "@server/utils/logger" import {clearDocumentSettings, getDocumentSettings, ServerSettings} from "@server/settings/settings" @@ -1124,6 +1130,48 @@ connection.onInitialize(async (initParams: lsp.InitializeParams): Promise { + const contracts: WorkspaceContractInfo[] = [] + + if (!workspaceFolders || workspaceFolders.length === 0) { + return {contracts: []} + } + + const workspaceIndexRoot = tolkIndex.roots.find(root => root.name === "workspace") + if (!workspaceIndexRoot) { + return {contracts: []} + } + + for (const [uri, fileIndex] of workspaceIndexRoot.files) { + const file = TOLK_PARSED_FILES_CACHE.get(uri) + if (!file) continue + + if (fileIndex.elementByName(IndexKey.Funcs, "onInternalMessage") === null) { + // not a root contract file + continue + } + + try { + const abi = contractAbi(file) + if (!abi) continue + + const filePath = fileURLToPath(uri) + const contractName = path.basename(filePath, path.extname(filePath)) + + contracts.push({ + name: contractName, + path: uri, + abi, + }) + } catch { + console.log(`Cannot get contract abi for ${file.uri}`) + // do nothing + } + } + + return {contracts} + }) + connection.onRequest(DocumentationAtPositionRequest, provideDocumentation) console.info("TON language server is ready!") diff --git a/shared/src/shared-msgtypes.ts b/shared/src/shared-msgtypes.ts index 37733492..97e2bdc7 100644 --- a/shared/src/shared-msgtypes.ts +++ b/shared/src/shared-msgtypes.ts @@ -8,6 +8,7 @@ export const TypeAtPositionRequest = "tolk.getTypeAtPosition" export const DocumentationAtPositionRequest = "tolk.executeHoverProvider" export const SetToolchainVersionNotification = "tolk.setToolchainVersion" export const ContractAbiRequest = "tolk.getContractAbi" +export const GetAllContractsAbiRequest = "tolk.getWorkspaceContractsAbi" export interface TypeAtPositionParams { readonly textDocument: { @@ -29,6 +30,16 @@ export interface GetContractAbiResponse { readonly abi: ContractAbi | undefined } +export interface GetWorkspaceContractsAbiResponse { + readonly contracts: WorkspaceContractInfo[] +} + +export interface WorkspaceContractInfo { + readonly name: string + readonly path: string + readonly abi: ContractAbi +} + export interface EnvironmentInfo { readonly nodeVersion?: string readonly platform: string diff --git a/webpack.config.js b/webpack.config.js index e70ac3d0..1c506b9d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -124,6 +124,7 @@ const webviewConfig = { "transaction-details": "./editors/code/src/webview-ui/src/views/details/transaction-details-main.tsx", history: "./editors/code/src/webview-ui/src/views/history/history-main.tsx", + tests: "./editors/code/src/webview-ui/src/views/tests/tests-main.tsx", }, output: { path: path.join(distDir, "webview-ui"), diff --git a/yarn.lock b/yarn.lock index 4d023eab..50df8d3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2060,6 +2060,40 @@ __metadata: languageName: node linkType: hard +"@lit-labs/ssr-dom-shim@npm:^1.4.0": + version: 1.4.0 + resolution: "@lit-labs/ssr-dom-shim@npm:1.4.0" + checksum: 10c0/eb8b4c6ed83db48e2f2c8c038f88e0ac302214918e5c1209458cb82a35ce27ce586100c5692885b2c5520f6941b2c3512f26c4d7b7dd48f13f17f1668553395a + languageName: node + linkType: hard + +"@lit/context@npm:^1.1.3": + version: 1.1.6 + resolution: "@lit/context@npm:1.1.6" + dependencies: + "@lit/reactive-element": "npm:^1.6.2 || ^2.1.0" + checksum: 10c0/203f761eda19c8b37d77f01d9a0148535c5b28c47b76e28b321cb6e5ed0546c45332512e68b1647ce92ca67690d8c66de31972ec115410b080d69fa25a5d86f3 + languageName: node + linkType: hard + +"@lit/react@npm:^1.0.7": + version: 1.0.8 + resolution: "@lit/react@npm:1.0.8" + peerDependencies: + "@types/react": 17 || 18 || 19 + checksum: 10c0/18bf3eb6584fa989e0ad40988b349a4401da1cecd5bf1c6edfc1c5caed80037852a4ebe5685b04941e5b28ccf93e740676dae32773d7ae44b1479b96538392b1 + languageName: node + linkType: hard + +"@lit/reactive-element@npm:^1.6.2 || ^2.1.0, @lit/reactive-element@npm:^2.1.0": + version: 2.1.1 + resolution: "@lit/reactive-element@npm:2.1.1" + dependencies: + "@lit-labs/ssr-dom-shim": "npm:^1.4.0" + checksum: 10c0/200d72c3d1bb8babc88123f3684e52cf490ec20cc7974002d666b092afa18e4a7c1ca15883c84c0b8671361a9875905eb18c1f03d20ecbbbaefdaec6e0c7c4eb + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -2662,6 +2696,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.2": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^3.0.0": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" @@ -2683,6 +2724,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8": + version: 8.18.1 + resolution: "@types/ws@npm:8.18.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/61aff1129143fcc4312f083bc9e9e168aa3026b7dd6e70796276dcfb2c8211c4292603f9c4864fae702f2ed86e4abd4d38aa421831c2fd7f856c931a481afbab + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -2989,6 +3039,29 @@ __metadata: languageName: node linkType: hard +"@vscode-elements/elements@npm:^2.3.0": + version: 2.3.1 + resolution: "@vscode-elements/elements@npm:2.3.1" + dependencies: + "@lit/context": "npm:^1.1.3" + lit: "npm:^3.2.1" + checksum: 10c0/296e98c12680a1459689be636962687207ecfac622608b3cdfc44cf9b08d462c2ee271efb54357136eca4d33ebcdf7492c0c174d16181c70ccb9dc3ba7710223 + languageName: node + linkType: hard + +"@vscode-elements/react-elements@npm:^2.3.1": + version: 2.3.1 + resolution: "@vscode-elements/react-elements@npm:2.3.1" + dependencies: + "@lit/react": "npm:^1.0.7" + "@vscode-elements/elements": "npm:^2.3.0" + peerDependencies: + react: 17 || 18 || 19 + react-dom: 17 || 18 || 19 + checksum: 10c0/6762f487a87e3d50a5cf7935537e7fcc59a9e1b2e563af1eb5cfae25b99a15c63d531d3418708a27869fccb9a290ff97387bee49adf5a287fdbb23cd1ff723de + languageName: node + linkType: hard + "@vscode/debugadapter@npm:^1.51.0": version: 1.68.0 resolution: "@vscode/debugadapter@npm:1.68.0" @@ -8160,6 +8233,37 @@ __metadata: languageName: node linkType: hard +"lit-element@npm:^4.2.0": + version: 4.2.1 + resolution: "lit-element@npm:4.2.1" + dependencies: + "@lit-labs/ssr-dom-shim": "npm:^1.4.0" + "@lit/reactive-element": "npm:^2.1.0" + lit-html: "npm:^3.3.0" + checksum: 10c0/2cb30cc7c5a006cd7995f882c5e9ed201638dc3513fdee989dd7b78d8ceb201cf6930abe5ebc5185d7fc3648933a6b6187742d5534269961cd20b9a78617068d + languageName: node + linkType: hard + +"lit-html@npm:^3.3.0": + version: 3.3.1 + resolution: "lit-html@npm:3.3.1" + dependencies: + "@types/trusted-types": "npm:^2.0.2" + checksum: 10c0/0dfb645f35c2ae129a40c09550b4d0e60617b715af7f2e0b825cdfd0624118fc4bf16e9cfaabdfbe43469522e145057d3cc46c64ca1019681480e4b9ae8f52cd + languageName: node + linkType: hard + +"lit@npm:^3.2.1": + version: 3.3.1 + resolution: "lit@npm:3.3.1" + dependencies: + "@lit/reactive-element": "npm:^2.1.0" + lit-element: "npm:^4.2.0" + lit-html: "npm:^3.3.0" + checksum: 10c0/9f3e171e211be7cd3e01693eac4ba4752fc7bafebc8298fc5b9cb70ff279dd4dc292f1cb425ca42f61c3767a75b7557295c2f6b16335719bc8cf1ca6f3622fb7 + languageName: node + linkType: hard + "loader-runner@npm:^4.2.0": version: 4.3.0 resolution: "loader-runner@npm:4.3.0" @@ -12255,6 +12359,8 @@ __metadata: "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" "@types/vscode": "npm:^1.63.0" + "@types/ws": "npm:^8" + "@vscode-elements/react-elements": "npm:^2.3.1" "@vscode/debugadapter": "npm:^1.51.0" "@vscode/debugprotocol": "npm:^1.68.0" "@vscode/test-cli": "npm:^0.0.10" @@ -12308,6 +12414,7 @@ __metadata: web-tree-sitter: "npm:^0.25.0" webpack: "npm:^5.92.1" webpack-cli: "npm:^5.1.4" + ws: "npm:^8.18.3" peerDependencies: tree-sitter: ^0.21.1 dependenciesMeta: @@ -12641,6 +12748,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.3": + version: 8.18.3 + resolution: "ws@npm:8.18.3" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + languageName: node + linkType: hard + "wsl-utils@npm:^0.1.0": version: 0.1.0 resolution: "wsl-utils@npm:0.1.0" From a36f0c774374a438b6fa1c6c050d23c1e687a426 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:18:08 +0400 Subject: [PATCH 02/16] better tree --- editors/code/src/extension.ts | 1 + .../src/providers/sandbox/TestTreeProvider.ts | 141 +++++++++++++++--- 2 files changed, 120 insertions(+), 22 deletions(-) diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 55524857..3cad6802 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -43,6 +43,7 @@ import {BocDecompilerProvider} from "./providers/boc/BocDecompilerProvider" import {registerSaveBocDecompiledCommand} from "./commands/saveBocDecompiledCommand" import {registerSandboxCommands, openFileAtPosition} from "./commands/sandboxCommands" import {parseCallStack} from "./common/call-stack-parser" + import {SandboxTreeProvider} from "./providers/sandbox/SandboxTreeProvider" import {SandboxActionsProvider} from "./providers/sandbox/SandboxActionsProvider" import {HistoryWebviewProvider} from "./providers/sandbox/HistoryWebviewProvider" diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index 46e7eb71..2d848dd3 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -2,6 +2,8 @@ // Copyright © 2025 TON Core import * as vscode from "vscode" +import {parseCallStack} from "../../common/call-stack-parser" + import {processTxString, TestDataMessage, TestRun} from "./test-types" interface TestTreeItem { @@ -60,12 +62,11 @@ export class TestTreeProvider implements vscode.TreeDataProvider { public getChildren(element?: TestTreeItem): Thenable { if (!element) { - // Корень дерева: возвращаем уникальные имена тестов return Promise.resolve( [...this.testRunsByName.keys()].map(testName => ({ id: `test-group-${testName}`, label: testName, - description: `${this.testRunsByName.get(testName)?.length ?? 0} runs`, + description: `${this.testRunsByName.get(testName)?.length ?? 0} transactions`, contextValue: "testGroup", iconPath: new vscode.ThemeIcon("beaker"), collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, @@ -75,29 +76,42 @@ export class TestTreeProvider implements vscode.TreeDataProvider { } if (element.type === "testName") { - // Для имени теста возвращаем test run как "Transaction #1", "#2", etc. const testRuns = this.testRunsByName.get(element.label) ?? [] - console.log("testRuns", testRuns.length) - return Promise.resolve( - testRuns.map((testRun, index) => ({ - id: `${element.id}-run-${index}`, - label: `Transaction #${index}`, - description: new Date(testRun.timestamp).toLocaleTimeString(), - contextValue: "testRun", - iconPath: new vscode.ThemeIcon("check"), - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - type: "testRun" as const, - command: { - command: "ton.test.showTransactionDetails", - title: "Show Test Run Details", - arguments: [testRun], - }, - })), + return Promise.all( + testRuns.map(async (testRun, index) => { + const extractedName = await extractTransactionName(testRun) + const exitCode = getTestRunExitCode(testRun) + const baseLabel = extractedName ?? `Transaction #${index}` + const label = exitCode === 0 ? baseLabel : `${baseLabel} (exit: ${exitCode})` + + return { + id: `${element.id}-run-${index}`, + label, + description: new Date(testRun.timestamp).toLocaleTimeString(), + contextValue: "testRun", + iconPath: + exitCode === 0 + ? new vscode.ThemeIcon( + "pass", + new vscode.ThemeColor("testing.iconPassed"), + ) + : new vscode.ThemeIcon( + "error", + new vscode.ThemeColor("testing.iconFailed"), + ), + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + type: "testRun" as const, + command: { + command: "ton.test.showTransactionDetails", + title: "Show Test Run Details", + arguments: [testRun], + }, + } + }), ) } if (element.type === "testRun") { - // Для test run возвращаем сообщения внутри транзакции const testName = element.id.split("-run-")[0].replace("test-group-", "") const runIndexStr = element.id.split("-run-")[1] const runIndex = Number.parseInt(runIndexStr) @@ -106,7 +120,6 @@ export class TestTreeProvider implements vscode.TreeDataProvider { const messageItems: TestTreeItem[] = [] - // Добавляем входящее сообщение, если оно есть if (testRun) { testRun.transactions.forEach((tx, txIndex) => { if (tx.transaction.inMessage) { @@ -121,7 +134,6 @@ export class TestTreeProvider implements vscode.TreeDataProvider { }) } - // Добавляем исходящие действия tx.outActions.forEach((action, actionIndex) => { messageItems.push({ id: `${element.id}-tx-${txIndex}-out-${actionIndex}`, @@ -167,3 +179,88 @@ export class TestTreeProvider implements vscode.TreeDataProvider { this._onDidChangeTreeData.fire(undefined) } } + +async function extractTransactionName(testRun: TestRun): Promise { + const transactionWithCallStack = testRun.transactions.find(tx => tx.callStack) + if (!transactionWithCallStack?.callStack) { + return undefined + } + + const parsedCallStack = parseCallStack(transactionWithCallStack.callStack) + if (parsedCallStack.length === 0) { + return undefined + } + + const lastEntry = parsedCallStack.at(-1) + if (!lastEntry?.file || lastEntry.line === undefined) { + return undefined + } + + try { + const uri = vscode.Uri.file(lastEntry.file) + const document = await vscode.workspace.openTextDocument(uri) + const lines = document.getText().split("\n") + + const lineIndex = lastEntry.line - 1 + if (lineIndex < 0 || lineIndex >= lines.length) { + return undefined + } + + const line = lines[lineIndex].trim() + + const patterns = [ + // await object.sendMethod( + /await\s+(\w+)\.(\w+)\s*\(/, + // object.sendMethod( + /(\w+)\.(\w+)\s*\(/, + // sendMethod( + /(\w+)\s*\(/, + ] + + for (const pattern of patterns) { + const match = line.match(pattern) + if (match) { + // Если нашли object.method, возвращаем object.method + if (match[2]) { + return `${match[1]}.${match[2]}` + } + // Иначе просто method + return match[1] + } + } + + for (let i = 1; i <= 3; i++) { + const prevLineIndex = lineIndex - i + if (prevLineIndex >= 0) { + const prevLine = lines[prevLineIndex].trim() + for (const pattern of patterns) { + const match = prevLine.match(pattern) + if (match) { + if (match[2]) { + return `${match[1]}.${match[2]}` + } + return match[1] + } + } + } + } + } catch (error) { + console.error("Error reading file for transaction name:", error) + } + + return undefined +} + +function getTestRunExitCode(testRun: TestRun): number { + const failedTransaction = testRun.transactions.find(tx => { + if (tx.computeInfo === "skipped") return false + const exitCode = tx.computeInfo.exitCode + return exitCode !== 0 && exitCode !== 1 + }) + + if (!failedTransaction || failedTransaction.computeInfo === "skipped") { + return 0 + } + + return failedTransaction.computeInfo.exitCode +} From 6a048dff1f4899103dcf50f4a7c17e86454164c1 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:22:03 +0400 Subject: [PATCH 03/16] cleanup --- editors/code/src/extension.ts | 12 +- .../providers/sandbox/TestWebviewProvider.ts | 127 -------------- .../src/providers/sandbox/WebSocketServer.ts | 9 +- .../src/views/tests/TestsApp.module.css | 116 ------------- .../webview-ui/src/views/tests/TestsApp.tsx | 67 -------- .../src/views/tests/components/TestsView.tsx | 161 ------------------ .../src/views/tests/hooks/useTestsApp.tsx | 64 ------- .../src/views/tests/sandbox-tests-types.ts | 36 ---- .../webview-ui/src/views/tests/tests-main.tsx | 23 --- package.json | 6 - webpack.config.js | 1 - 11 files changed, 4 insertions(+), 618 deletions(-) delete mode 100644 editors/code/src/providers/sandbox/TestWebviewProvider.ts delete mode 100644 editors/code/src/webview-ui/src/views/tests/TestsApp.module.css delete mode 100644 editors/code/src/webview-ui/src/views/tests/TestsApp.tsx delete mode 100644 editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx delete mode 100644 editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx delete mode 100644 editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts delete mode 100644 editors/code/src/webview-ui/src/views/tests/tests-main.tsx diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 3cad6802..94b61eaf 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -50,7 +50,6 @@ import {HistoryWebviewProvider} from "./providers/sandbox/HistoryWebviewProvider import {TransactionDetailsProvider} from "./providers/sandbox/TransactionDetailsProvider" import {SandboxCodeLensProvider} from "./providers/sandbox/SandboxCodeLensProvider" import {TestTreeProvider} from "./providers/sandbox/TestTreeProvider" -import {TestWebviewProvider} from "./providers/sandbox/TestWebviewProvider" import {WebSocketServer} from "./providers/sandbox/WebSocketServer" import {configureDebugging} from "./debugging" @@ -119,14 +118,6 @@ export async function activate(context: vscode.ExtensionContext): Promise treeDataProvider: testTreeProvider, showCollapseAll: false, }), - ) - - const testWebviewProvider = new TestWebviewProvider(context.extensionUri, testTreeProvider) - context.subscriptions.push( - vscode.window.registerWebviewViewProvider( - TestWebviewProvider.viewType, - testWebviewProvider, - ), vscode.commands.registerCommand( "ton.test.showTransactionDetails", async (testRun: TestRun) => { @@ -238,9 +229,8 @@ export async function activate(context: vscode.ExtensionContext): Promise sandboxTreeProvider.setActionsProvider(sandboxActionsProvider) sandboxTreeProvider.setCodeLensProvider(sandboxCodeLensProvider) - const wsServer = new WebSocketServer() + const wsServer = new WebSocketServer(testTreeProvider) wsServer.start() - wsServer.setTestWebviewProvider(testWebviewProvider) context.subscriptions.push( { diff --git a/editors/code/src/providers/sandbox/TestWebviewProvider.ts b/editors/code/src/providers/sandbox/TestWebviewProvider.ts deleted file mode 100644 index f49b4262..00000000 --- a/editors/code/src/providers/sandbox/TestWebviewProvider.ts +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Core -import * as vscode from "vscode" - -import {TestTreeProvider} from "./TestTreeProvider" -import {TestDataMessage} from "./test-types" - -interface TestCommand { - readonly type: "clearAllTests" | "removeTest" | "showTransactionDetails" - readonly testId?: string - readonly testRunId?: string - readonly transactionId?: string -} - -interface TestMessage { - readonly type: "addTestData" - readonly data: TestDataMessage -} - -export class TestWebviewProvider implements vscode.WebviewViewProvider { - public static readonly viewType: string = "tonTestResults" - - private view?: vscode.WebviewView - - public constructor( - private readonly _extensionUri: vscode.Uri, - private readonly _treeProvider: TestTreeProvider, - ) {} - - public resolveWebviewView( - webviewView: vscode.WebviewView, - _context: vscode.WebviewViewResolveContext, - _token: vscode.CancellationToken, - ): void { - this.view = webviewView - - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [this._extensionUri], - } - - webviewView.webview.html = this.getHtmlForWebview(webviewView.webview) - - webviewView.webview.onDidReceiveMessage((command: TestCommand) => { - switch (command.type) { - case "clearAllTests": { - this._treeProvider.clearAllTests() - break - } - case "removeTest": { - if (command.testId) { - this._treeProvider.removeTestRun(command.testId) - } - break - } - case "showTransactionDetails": { - if (command.testRunId && command.transactionId) { - const testRun = this._treeProvider.getTestRun(command.testRunId) - if (testRun) { - // const transaction = testRun.transactions.find( - // tx => tx.id === command.transactionId, - // ) - // if (transaction) { - // // Здесь можно открыть детали транзакции - // vscode.window.showInformationMessage( - // `Transaction Details: ${transaction.address} (LT: ${transaction.lt})`, - // ) - // } - } - } - break - } - default: { - console.warn("Unknown command type:", command.type) - } - } - }) - } - - public addTestData(data: TestDataMessage): void { - this._treeProvider.addTestData(data) - - // Отправляем данные в webview - if (this.view) { - const message: TestMessage = { - type: "addTestData", - data, - } - void this.view.webview.postMessage(message) - } - } - - private getHtmlForWebview(webview: vscode.Webview): string { - const scriptUri = webview.asWebviewUri( - vscode.Uri.joinPath(this._extensionUri, "dist", "webview-ui", "tests.js"), - ) - const styleUri = webview.asWebviewUri( - vscode.Uri.joinPath(this._extensionUri, "dist", "webview-ui", "tests.css"), - ) - - const nonce = getNonce() - - return ` - - - - - - - Test Results - - -
- - - ` - } -} - -function getNonce(): string { - let text = "" - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)) - } - return text -} diff --git a/editors/code/src/providers/sandbox/WebSocketServer.ts b/editors/code/src/providers/sandbox/WebSocketServer.ts index b2b38b4c..a4308fcb 100644 --- a/editors/code/src/providers/sandbox/WebSocketServer.ts +++ b/editors/code/src/providers/sandbox/WebSocketServer.ts @@ -2,23 +2,20 @@ import * as vscode from "vscode" import {WebSocket, Server} from "ws" import {TestDataMessage} from "./test-types" +import {TestTreeProvider} from "./TestTreeProvider" export class WebSocketServer { private wss: Server | null = null private disposables: vscode.Disposable[] = [] - private testWebviewProvider?: {addTestData: (data: TestDataMessage) => void} public constructor( + private readonly treeProvider: TestTreeProvider, private readonly port: number = Number.parseInt( process.env.VSCODE_WEBSOCKET_PORT ?? "7743", 10, ), ) {} - public setTestWebviewProvider(provider: {addTestData: (data: TestDataMessage) => void}): void { - this.testWebviewProvider = provider - } - public start(): void { try { this.wss = new Server({port: this.port}) @@ -56,7 +53,7 @@ export class WebSocketServer { } private handleTestData(message: TestDataMessage): void { - this.testWebviewProvider?.addTestData(message) + this.treeProvider.addTestData(message) } public stop(): void { diff --git a/editors/code/src/webview-ui/src/views/tests/TestsApp.module.css b/editors/code/src/webview-ui/src/views/tests/TestsApp.module.css deleted file mode 100644 index 67dd9af1..00000000 --- a/editors/code/src/webview-ui/src/views/tests/TestsApp.module.css +++ /dev/null @@ -1,116 +0,0 @@ -.container { - height: 100vh; - display: flex; - flex-direction: column; -} - -.header { - padding: 12px 16px; - border-bottom: 1px solid var(--vscode-panel-border); - background-color: var(--vscode-editor-background); -} - -.header h2 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: var(--vscode-foreground); -} - -.content { - flex: 1; - overflow: auto; -} - -.testItem { - display: flex; - align-items: center; - padding: 8px 16px; - border-bottom: 1px solid var(--vscode-list-inactiveSelectionBackground); - cursor: pointer; -} - -.testItem:hover { - background-color: var(--vscode-list-hoverBackground); -} - -.testIcon { - margin-right: 8px; - color: var(--vscode-foreground); -} - -.testInfo { - flex: 1; -} - -.testName { - font-size: 13px; - font-weight: 500; - color: var(--vscode-foreground); -} - -.testTime { - font-size: 11px; - color: var(--vscode-descriptionForeground); -} - -.transactionItem { - display: flex; - align-items: center; - padding: 6px 16px 6px 32px; - border-bottom: 1px solid var(--vscode-list-inactiveSelectionBackground); - cursor: pointer; -} - -.transactionItem:hover { - background-color: var(--vscode-list-hoverBackground); -} - -.transactionIcon { - margin-right: 8px; -} - -.transactionInfo { - flex: 1; -} - -.transactionAddress { - font-size: 12px; - font-family: var(--vscode-editor-font-family); - color: var(--vscode-foreground); -} - -.transactionLt { - font-size: 10px; - color: var(--vscode-descriptionForeground); -} - -.toolbar { - display: flex; - gap: 8px; - padding: 8px 16px; - border-bottom: 1px solid var(--vscode-panel-border); -} - -.button { - padding: 4px 8px; - font-size: 11px; - border: 1px solid var(--vscode-button-border); - background-color: var(--vscode-button-background); - color: var(--vscode-button-foreground); - border-radius: 3px; - cursor: pointer; -} - -.button:hover { - background-color: var(--vscode-button-hoverBackground); -} - -.button.secondary { - background-color: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); -} - -.button.secondary:hover { - background-color: var(--vscode-button-secondaryHoverBackground); -} diff --git a/editors/code/src/webview-ui/src/views/tests/TestsApp.tsx b/editors/code/src/webview-ui/src/views/tests/TestsApp.tsx deleted file mode 100644 index 2e2ab22d..00000000 --- a/editors/code/src/webview-ui/src/views/tests/TestsApp.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Core -import React, {useCallback, useEffect, useState} from "react" - -import {TestDataMessage} from "../../../../providers/sandbox/test-types" - -import {TestsView} from "./components/TestsView" -import {TestsAppProvider} from "./hooks/useTestsApp" -import {TestsVSCodeAPI, TestsCommand} from "./sandbox-tests-types" - -import "./TestsApp.module.css" - -interface TestsMessage { - readonly type: "addTestData" - readonly data: TestDataMessage -} - -interface TestsAppProps { - readonly vscode: TestsVSCodeAPI -} - -export function TestsApp({vscode}: TestsAppProps): React.JSX.Element { - const [messages, setMessages] = useState([]) - - const postMessage = useCallback( - (message: TestsCommand) => { - vscode.postMessage(message) - }, - [vscode], - ) - - useEffect(() => { - const handler = (event: MessageEvent): void => { - const message = event.data as TestsMessage - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (message.type === "addTestData") { - setMessages(prev => [...prev, message]) - } - } - - window.addEventListener("message", handler) - return () => { - window.removeEventListener("message", handler) - } - }, []) - - const handleClearAllTests = useCallback(() => { - postMessage({type: "clearAllTests"}) - }, [postMessage]) - - const handleRemoveTest = useCallback( - (testId: string) => { - postMessage({type: "removeTest", testId}) - }, - [postMessage], - ) - - return ( - - - - ) -} diff --git a/editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx b/editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx deleted file mode 100644 index 50d1ddc7..00000000 --- a/editors/code/src/webview-ui/src/views/tests/components/TestsView.tsx +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Core -import React, {useCallback, useEffect, useState} from "react" - -import {useTestsApp} from "../hooks/useTestsApp" - -import styles from "../TestsApp.module.css" -import {TestDataMessage} from "../../../../../providers/sandbox/test-types" - -interface TestsViewProps { - readonly messages: readonly {type: string; data?: TestDataMessage}[] - readonly onClearAllTests: () => void - readonly onRemoveTest: (testId: string) => void -} - -export function TestsView({ - messages, - onClearAllTests, - onRemoveTest, -}: TestsViewProps): React.JSX.Element { - const {testRuns, addTestData} = useTestsApp() - const [expandedTests, setExpandedTests] = useState>(new Set()) - - // Обработка входящих сообщений - useEffect(() => { - messages.forEach(message => { - if (message.type === "addTestData" && message.data) { - addTestData(message.data) - } - }) - }, [messages, addTestData]) - - const toggleTestExpansion = useCallback((testId: string) => { - setExpandedTests(prev => { - const newSet = new Set(prev) - if (newSet.has(testId)) { - newSet.delete(testId) - } else { - newSet.add(testId) - } - return newSet - }) - }, []) - - const handleTransactionClick = useCallback((testRunId: string, transactionId: string) => { - // Отправляем сообщение в extension для открытия деталей транзакции - // vscode API будет передан через контекст или пропс в будущем - console.log("Show transaction details:", testRunId, transactionId) - }, []) - - return ( -
-
-

Test Results

-
- -
- - - {testRuns.length} test run{testRuns.length === 1 ? "" : "s"} - -
- -
- {testRuns.length === 0 ? ( -
- No test runs yet. Run your tests to see results here. -
- ) : ( - testRuns.map(testRun => ( -
-
{ - toggleTestExpansion(testRun.id) - }} - role="button" - tabIndex={0} - onKeyDown={e => { - if (e.key === "Enter" || e.key === " ") { - toggleTestExpansion(testRun.id) - } - }} - > - -
-
{testRun.name}
-
- {new Date(testRun.timestamp).toLocaleString()} -
-
- -
- - {expandedTests.has(testRun.id) && ( -
- {testRun.transactions.map(transaction => ( -
{ - handleTransactionClick(testRun.id, transaction.transaction.lt.toString()) - }} - role="button" - tabIndex={0} - onKeyDown={e => { - if (e.key === "Enter" || e.key === " ") { - handleTransactionClick(testRun.id, transaction.transaction.lt.toString()) - } - }} - > - -
-
- {transaction.address?.toString()} -
-
- LT: {transaction.transaction.lt.toString()} -
-
-
- ))} -
- )} -
- )) - )} -
-
- ) -} diff --git a/editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx b/editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx deleted file mode 100644 index f9448a42..00000000 --- a/editors/code/src/webview-ui/src/views/tests/hooks/useTestsApp.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Core -import React, {createContext, ReactNode, useCallback, useContext, useMemo, useState} from "react" - -import { - processTxString, - TestDataMessage, - TestRun, -} from "../../../../../providers/sandbox/test-types" - -interface TestsAppContextValue { - readonly testRuns: readonly TestRun[] - readonly addTestData: (data: TestDataMessage) => void - readonly clearAllTests: () => void - readonly removeTest: (testId: string) => void -} - -const TestsAppContext = createContext(null) - -export function TestsAppProvider({children}: {children: ReactNode}): React.JSX.Element { - const [testRuns, setTestRuns] = useState([]) - - const addTestData = useCallback((data: TestDataMessage) => { - const testRun: TestRun = { - id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, - name: data.testName, - timestamp: Date.now(), - transactions: processTxString(data.transactions), - contracts: data.contracts, - changes: data.changes, - resultString: data.transactions, - } - - setTestRuns(prev => [testRun, ...prev.slice(0, 49)]) // Ограничиваем до 50 тестов - }, []) - - const clearAllTests = useCallback(() => { - setTestRuns([]) - }, []) - - const removeTest = useCallback((testId: string) => { - setTestRuns(prev => prev.filter(run => run.id !== testId)) - }, []) - - const value = useMemo( - () => ({ - testRuns, - addTestData, - clearAllTests, - removeTest, - }), - [testRuns, addTestData, clearAllTests, removeTest], - ) - - return {children} -} - -export function useTestsApp(): TestsAppContextValue { - const context = useContext(TestsAppContext) - if (!context) { - throw new Error("useTestsApp must be used within TestsAppProvider") - } - return context -} diff --git a/editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts b/editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts deleted file mode 100644 index 5c20bc1d..00000000 --- a/editors/code/src/webview-ui/src/views/tests/sandbox-tests-types.ts +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Core - -import {TestDataMessage} from "../../../../providers/sandbox/test-types" - -export interface TestsVSCodeAPI { - readonly postMessage: (command: TestsCommand) => void - readonly getState: () => unknown - readonly setState: (state: unknown) => void -} - -export interface AddTestDataCommand { - readonly type: "addTestData" - readonly data: TestDataMessage -} - -export interface ClearAllTestsCommand { - readonly type: "clearAllTests" -} - -export interface RemoveTestCommand { - readonly type: "removeTest" - readonly testId: string -} - -export interface ShowTransactionDetailsCommand { - readonly type: "showTransactionDetails" - readonly testRunId: string - readonly transactionId: string -} - -export type TestsCommand = - | AddTestDataCommand - | ClearAllTestsCommand - | RemoveTestCommand - | ShowTransactionDetailsCommand diff --git a/editors/code/src/webview-ui/src/views/tests/tests-main.tsx b/editors/code/src/webview-ui/src/views/tests/tests-main.tsx deleted file mode 100644 index 004e3a43..00000000 --- a/editors/code/src/webview-ui/src/views/tests/tests-main.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright © 2025 TON Core -import React from "react" -import {createRoot} from "react-dom/client" - -import {TestsApp} from "./TestsApp" -import {TestsVSCodeAPI} from "./sandbox-tests-types" - -declare function acquireVsCodeApi(): TestsVSCodeAPI - -const vscode = acquireVsCodeApi() - -const container = document.querySelector("#root") -if (!container) { - throw new Error("Root element not found") -} - -const root = createRoot(container) -root.render( - - - , -) diff --git a/package.json b/package.json index 0ab7924f..dceb0e12 100644 --- a/package.json +++ b/package.json @@ -415,12 +415,6 @@ "id": "tonTestResultsTree", "name": "Tests Tree", "when": "true" - }, - { - "id": "tonTestResults", - "type": "webview", - "name": "Test Results", - "when": "true" } ] }, diff --git a/webpack.config.js b/webpack.config.js index 1c506b9d..e70ac3d0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -124,7 +124,6 @@ const webviewConfig = { "transaction-details": "./editors/code/src/webview-ui/src/views/details/transaction-details-main.tsx", history: "./editors/code/src/webview-ui/src/views/history/history-main.tsx", - tests: "./editors/code/src/webview-ui/src/views/tests/tests-main.tsx", }, output: { path: path.join(distDir, "webview-ui"), From a1c870872590caa17597a52c01b8873621978ee5 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:31:34 +0400 Subject: [PATCH 04/16] show welcome view for empty tree --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index dceb0e12..921f2600 100644 --- a/package.json +++ b/package.json @@ -791,6 +791,12 @@ "type": "blueprint-build-and-test-all", "properties": {} } + ], + "viewsWelcome": [ + { + "view": "tonTestResultsTree", + "contents": "No test results yet. Run tests to see results here." + } ] }, "dependencies": { From ee60169474444587ffb8ee952c8ccb7c80a4c8c1 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:35:08 +0400 Subject: [PATCH 05/16] cleanup --- .../src/providers/sandbox/TestTreeProvider.ts | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index 2d848dd3..44c43786 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -99,7 +99,7 @@ export class TestTreeProvider implements vscode.TreeDataProvider { "error", new vscode.ThemeColor("testing.iconFailed"), ), - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + collapsibleState: vscode.TreeItemCollapsibleState.None, type: "testRun" as const, command: { command: "ton.test.showTransactionDetails", @@ -111,46 +111,6 @@ export class TestTreeProvider implements vscode.TreeDataProvider { ) } - if (element.type === "testRun") { - const testName = element.id.split("-run-")[0].replace("test-group-", "") - const runIndexStr = element.id.split("-run-")[1] - const runIndex = Number.parseInt(runIndexStr) - const testRuns = this.testRunsByName.get(testName) ?? [] - const testRun = testRuns.at(runIndex) // Обратный порядок из-за unshift - - const messageItems: TestTreeItem[] = [] - - if (testRun) { - testRun.transactions.forEach((tx, txIndex) => { - if (tx.transaction.inMessage) { - messageItems.push({ - id: `${element.id}-tx-${txIndex}-in`, - label: `Incoming Message`, - description: `To: ${tx.address?.toString().slice(0, 10) ?? "unknown"}...`, - contextValue: "message", - iconPath: new vscode.ThemeIcon("arrow-right"), - collapsibleState: vscode.TreeItemCollapsibleState.None, - type: "message" as const, - }) - } - - tx.outActions.forEach((action, actionIndex) => { - messageItems.push({ - id: `${element.id}-tx-${txIndex}-out-${actionIndex}`, - label: `Outgoing Action: ${action.type}`, - description: action.type === "sendMsg" ? "Send Message" : action.type, - contextValue: "message", - iconPath: new vscode.ThemeIcon("arrow-left"), - collapsibleState: vscode.TreeItemCollapsibleState.None, - type: "message" as const, - }) - }) - }) - } - - return Promise.resolve(messageItems) - } - return Promise.resolve([]) } From fb4e0b822abf191f44bc1113f3a747881fb84061 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:44:30 +0400 Subject: [PATCH 06/16] add webocket setting --- editors/code/src/extension.ts | 5 ++++- editors/code/src/providers/sandbox/WebSocketServer.ts | 5 +---- package.json | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 94b61eaf..f606bf5e 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -229,7 +229,10 @@ export async function activate(context: vscode.ExtensionContext): Promise sandboxTreeProvider.setActionsProvider(sandboxActionsProvider) sandboxTreeProvider.setCodeLensProvider(sandboxCodeLensProvider) - const wsServer = new WebSocketServer(testTreeProvider) + const websocketPort = vscode.workspace + .getConfiguration("ton.sandbox") + .get("websocketPort", 7743) + const wsServer = new WebSocketServer(testTreeProvider, websocketPort) wsServer.start() context.subscriptions.push( diff --git a/editors/code/src/providers/sandbox/WebSocketServer.ts b/editors/code/src/providers/sandbox/WebSocketServer.ts index a4308fcb..eebf9ee2 100644 --- a/editors/code/src/providers/sandbox/WebSocketServer.ts +++ b/editors/code/src/providers/sandbox/WebSocketServer.ts @@ -10,10 +10,7 @@ export class WebSocketServer { public constructor( private readonly treeProvider: TestTreeProvider, - private readonly port: number = Number.parseInt( - process.env.VSCODE_WEBSOCKET_PORT ?? "7743", - 10, - ), + private readonly port: number, ) {} public start(): void { diff --git a/package.json b/package.json index 921f2600..40256642 100644 --- a/package.json +++ b/package.json @@ -758,6 +758,11 @@ "type": "string", "default": "./node_modules/.bin/ton-sandbox-server", "description": "Path to the TON Sandbox server binary" + }, + "ton.sandbox.websocketPort": { + "type": "number", + "default": 7743, + "description": "Port for the WebSocket server used for test communication" } } } From 18f85b2b4a95351e490f282b8ea3bcb12674f99b Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:05:11 +0400 Subject: [PATCH 07/16] simplify --- .../src/providers/sandbox/TestTreeProvider.ts | 2 -- .../TransactionTree.module.css | 22 --------------- .../TransactionTree/TransactionTree.tsx | 27 ------------------- 3 files changed, 51 deletions(-) diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index 44c43786..eb72ed4b 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -28,8 +28,6 @@ export class TestTreeProvider implements vscode.TreeDataProvider { public addTestData(data: TestDataMessage): void { const transactions = processTxString(data.transactions) - console.log(data, "with", transactions.length, "transactions") - const testRun: TestRun = { id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, name: data.testName, diff --git a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css index a16c2cfc..b3cb573b 100644 --- a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css +++ b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css @@ -177,28 +177,6 @@ gap: var(--spacing-xs); } -.callStackButton { - background: none; - border: none; - cursor: pointer; - padding: 2px; - color: var(--color-text-tertiary); - opacity: 0.7; - transition: opacity 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--border-radius-sm); - position: absolute; - top: -2px; - right: -2px; -} - -.callStackButton:hover { - opacity: 1; - background-color: var(--color-background-hover); -} - .bottonText { display: flex; flex-direction: column; diff --git a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx index 1122cf4f..47ff0324 100644 --- a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx +++ b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx @@ -2,7 +2,6 @@ import React, {useMemo, useState, useRef, useEffect} from "react" import {Orientation, RawNodeDatum, TreeLinkDatum, Tree} from "react-d3-tree" import {Address} from "@ton/core" -import {FiExternalLink} from "react-icons/fi" import {TransactionDetails} from "../index" @@ -14,8 +13,6 @@ import {ParsedDataView} from "../ParsedDataView/ParsedDataView" import {parseData, ParsedObject} from "../../../../../../common/binary" -import {CallStackEntry, parseCallStack} from "../../../../../../common/call-stack-parser" - import {VSCodeTransactionDetailsAPI} from "../../transaction-details-types" import {useTooltip} from "./useTooltip" @@ -36,7 +33,6 @@ interface TransactionTooltipData { readonly totalFees: bigint } readonly sentTotal: bigint - readonly callStackPosition?: CallStackEntry } interface TransactionTreeProps { @@ -125,23 +121,6 @@ function TransactionTooltipContent({ )} - - {data.callStackPosition && ( - - )} ) } @@ -290,14 +269,11 @@ export function TransactionTree({ const srcAddress = tx.transaction.inMessage?.info.src const fromAddressStr = srcAddress ? formatAddressShort(srcAddress as Address) : "unknown" - const parsedCallStack = parseCallStack(tx.callStack) - const tooltipData: TransactionTooltipData = { fromAddress: fromAddressStr, computePhase, fees, sentTotal: tx.money.sentTotal, - callStackPosition: parsedCallStack.length > 0 ? parsedCallStack.at(-1) : undefined, } showTooltip({ @@ -388,9 +364,6 @@ export function TransactionTree({ const lt = tx.transaction.lt.toString() const isSelected = selectedTransaction?.transaction.lt.toString() === lt - const parsedCallStack = parseCallStack(tx.callStack) - const firstEntry = parsedCallStack.length > 0 ? parsedCallStack[0] : undefined - const hasExternalOut = tx.transaction.outMessages.values().some(outMsg => { return outMsg.info.type === "external-out" }) From 448ff86b59677c081c1d46902f249cfd8ef86ec9 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:05:42 +0400 Subject: [PATCH 08/16] dedupe --- yarn.lock | 96 ------------------------------------------------------- 1 file changed, 96 deletions(-) diff --git a/yarn.lock b/yarn.lock index 50df8d3f..4073b103 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2060,40 +2060,6 @@ __metadata: languageName: node linkType: hard -"@lit-labs/ssr-dom-shim@npm:^1.4.0": - version: 1.4.0 - resolution: "@lit-labs/ssr-dom-shim@npm:1.4.0" - checksum: 10c0/eb8b4c6ed83db48e2f2c8c038f88e0ac302214918e5c1209458cb82a35ce27ce586100c5692885b2c5520f6941b2c3512f26c4d7b7dd48f13f17f1668553395a - languageName: node - linkType: hard - -"@lit/context@npm:^1.1.3": - version: 1.1.6 - resolution: "@lit/context@npm:1.1.6" - dependencies: - "@lit/reactive-element": "npm:^1.6.2 || ^2.1.0" - checksum: 10c0/203f761eda19c8b37d77f01d9a0148535c5b28c47b76e28b321cb6e5ed0546c45332512e68b1647ce92ca67690d8c66de31972ec115410b080d69fa25a5d86f3 - languageName: node - linkType: hard - -"@lit/react@npm:^1.0.7": - version: 1.0.8 - resolution: "@lit/react@npm:1.0.8" - peerDependencies: - "@types/react": 17 || 18 || 19 - checksum: 10c0/18bf3eb6584fa989e0ad40988b349a4401da1cecd5bf1c6edfc1c5caed80037852a4ebe5685b04941e5b28ccf93e740676dae32773d7ae44b1479b96538392b1 - languageName: node - linkType: hard - -"@lit/reactive-element@npm:^1.6.2 || ^2.1.0, @lit/reactive-element@npm:^2.1.0": - version: 2.1.1 - resolution: "@lit/reactive-element@npm:2.1.1" - dependencies: - "@lit-labs/ssr-dom-shim": "npm:^1.4.0" - checksum: 10c0/200d72c3d1bb8babc88123f3684e52cf490ec20cc7974002d666b092afa18e4a7c1ca15883c84c0b8671361a9875905eb18c1f03d20ecbbbaefdaec6e0c7c4eb - languageName: node - linkType: hard - "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -2696,13 +2662,6 @@ __metadata: languageName: node linkType: hard -"@types/trusted-types@npm:^2.0.2": - version: 2.0.7 - resolution: "@types/trusted-types@npm:2.0.7" - checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c - languageName: node - linkType: hard - "@types/unist@npm:*, @types/unist@npm:^3.0.0": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" @@ -3039,29 +2998,6 @@ __metadata: languageName: node linkType: hard -"@vscode-elements/elements@npm:^2.3.0": - version: 2.3.1 - resolution: "@vscode-elements/elements@npm:2.3.1" - dependencies: - "@lit/context": "npm:^1.1.3" - lit: "npm:^3.2.1" - checksum: 10c0/296e98c12680a1459689be636962687207ecfac622608b3cdfc44cf9b08d462c2ee271efb54357136eca4d33ebcdf7492c0c174d16181c70ccb9dc3ba7710223 - languageName: node - linkType: hard - -"@vscode-elements/react-elements@npm:^2.3.1": - version: 2.3.1 - resolution: "@vscode-elements/react-elements@npm:2.3.1" - dependencies: - "@lit/react": "npm:^1.0.7" - "@vscode-elements/elements": "npm:^2.3.0" - peerDependencies: - react: 17 || 18 || 19 - react-dom: 17 || 18 || 19 - checksum: 10c0/6762f487a87e3d50a5cf7935537e7fcc59a9e1b2e563af1eb5cfae25b99a15c63d531d3418708a27869fccb9a290ff97387bee49adf5a287fdbb23cd1ff723de - languageName: node - linkType: hard - "@vscode/debugadapter@npm:^1.51.0": version: 1.68.0 resolution: "@vscode/debugadapter@npm:1.68.0" @@ -8233,37 +8169,6 @@ __metadata: languageName: node linkType: hard -"lit-element@npm:^4.2.0": - version: 4.2.1 - resolution: "lit-element@npm:4.2.1" - dependencies: - "@lit-labs/ssr-dom-shim": "npm:^1.4.0" - "@lit/reactive-element": "npm:^2.1.0" - lit-html: "npm:^3.3.0" - checksum: 10c0/2cb30cc7c5a006cd7995f882c5e9ed201638dc3513fdee989dd7b78d8ceb201cf6930abe5ebc5185d7fc3648933a6b6187742d5534269961cd20b9a78617068d - languageName: node - linkType: hard - -"lit-html@npm:^3.3.0": - version: 3.3.1 - resolution: "lit-html@npm:3.3.1" - dependencies: - "@types/trusted-types": "npm:^2.0.2" - checksum: 10c0/0dfb645f35c2ae129a40c09550b4d0e60617b715af7f2e0b825cdfd0624118fc4bf16e9cfaabdfbe43469522e145057d3cc46c64ca1019681480e4b9ae8f52cd - languageName: node - linkType: hard - -"lit@npm:^3.2.1": - version: 3.3.1 - resolution: "lit@npm:3.3.1" - dependencies: - "@lit/reactive-element": "npm:^2.1.0" - lit-element: "npm:^4.2.0" - lit-html: "npm:^3.3.0" - checksum: 10c0/9f3e171e211be7cd3e01693eac4ba4752fc7bafebc8298fc5b9cb70ff279dd4dc292f1cb425ca42f61c3767a75b7557295c2f6b16335719bc8cf1ca6f3622fb7 - languageName: node - linkType: hard - "loader-runner@npm:^4.2.0": version: 4.3.0 resolution: "loader-runner@npm:4.3.0" @@ -12360,7 +12265,6 @@ __metadata: "@types/react-dom": "npm:^18.3.0" "@types/vscode": "npm:^1.63.0" "@types/ws": "npm:^8" - "@vscode-elements/react-elements": "npm:^2.3.1" "@vscode/debugadapter": "npm:^1.51.0" "@vscode/debugprotocol": "npm:^1.68.0" "@vscode/test-cli": "npm:^0.0.10" From 1043792f6b15fe84fc9d39d20d2fb15ea25fc70d Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:10:57 +0400 Subject: [PATCH 09/16] rename --- editors/code/src/extension.ts | 57 +++++++----------- .../src/providers/sandbox/TestTreeProvider.ts | 60 ++++++------------- .../code/src/providers/sandbox/test-types.ts | 11 +--- 3 files changed, 39 insertions(+), 89 deletions(-) diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index f606bf5e..3d4bf953 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -53,7 +53,7 @@ import {TestTreeProvider} from "./providers/sandbox/TestTreeProvider" import {WebSocketServer} from "./providers/sandbox/WebSocketServer" import {configureDebugging} from "./debugging" -import {ContractData, TestRun} from "./providers/sandbox/test-types" +import {ContractData, TransactionRun} from "./providers/sandbox/test-types" import {TransactionDetailsInfo} from "./common/types/transaction" import {DeployedContract} from "./common/types/contract" import {Base64String} from "./common/base64-string" @@ -120,24 +120,16 @@ export async function activate(context: vscode.ExtensionContext): Promise }), vscode.commands.registerCommand( "ton.test.showTransactionDetails", - async (testRun: TestRun) => { + async (txRun: TransactionRun) => { const workspaceContracts = await vscode.commands.executeCommand( "tolk.getWorkspaceContractsAbi", ) - console.log("workspaceContracts", workspaceContracts.contracts.length) - console.log( - "workspaceContracts", - workspaceContracts.contracts.filter(it => it.name.includes("jetton")), - ) - - console.log(`txs of ${testRun.id}`, testRun.transactions) - // TODO: redone - const transaction = testRun.transactions.at(1) ?? testRun.transactions.at(0) + const transaction = txRun.transactions.at(1) ?? txRun.transactions.at(0) if (transaction) { - const contract = testRun.contracts.find( + const contract = txRun.contracts.find( it => it.address === transaction.address?.toString(), ) @@ -149,9 +141,6 @@ export async function activate(context: vscode.ExtensionContext): Promise sameNameContract(it, contract), ) const abi = workspaceContract?.abi - console.log("contract data", contract) - console.log("workspaceContract", workspaceContract) - console.log("abi", abi) const transactionDetailsInfo: TransactionDetailsInfo = { contractAddress: transaction.address?.toString() ?? "unknown", @@ -159,8 +148,8 @@ export async function activate(context: vscode.ExtensionContext): Promise transactionId: transaction.transaction.lt.toString(), timestamp: new Date().toISOString(), status: "success" as const, // For now, assume success - resultString: testRun.resultString, - deployedContracts: testRun.contracts.map((contract): DeployedContract => { + resultString: txRun.resultString, + deployedContracts: txRun.contracts.map((contract): DeployedContract => { const workspaceContract = workspaceContracts.contracts.find(it => sameNameContract(it, contract), ) @@ -193,28 +182,24 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.commands.registerCommand( "ton.test.openTestSource", (treeItem: {command?: vscode.Command}) => { - const testRun = treeItem.command?.arguments?.[0] as TestRun | undefined - if (!testRun) { - console.error("No testRun found in tree item arguments") + const txRun = treeItem.command?.arguments?.[0] as TransactionRun | undefined + if (!txRun) { + console.error("No txRun found in tree item arguments") return } - const transactionWithCallStack = testRun.transactions.find(tx => tx.callStack) - if (transactionWithCallStack?.callStack) { - const parsedCallStack = parseCallStack(transactionWithCallStack.callStack) - if (parsedCallStack.length > 0) { - const lastEntry = parsedCallStack.at(-1) - if ( - lastEntry?.file && - lastEntry.line !== undefined && - lastEntry.column !== undefined - ) { - openFileAtPosition( - lastEntry.file, - lastEntry.line - 1, - lastEntry.column - 1, - ) - } + const transactionWithCallStack = txRun.transactions.find(tx => tx.callStack) + if (!transactionWithCallStack?.callStack) return + + const parsedCallStack = parseCallStack(transactionWithCallStack.callStack) + if (parsedCallStack.length > 0) { + const lastEntry = parsedCallStack.at(-1) + if ( + lastEntry?.file && + lastEntry.line !== undefined && + lastEntry.column !== undefined + ) { + openFileAtPosition(lastEntry.file, lastEntry.line - 1, lastEntry.column - 1) } } }, diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index eb72ed4b..819ccfdf 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode" import {parseCallStack} from "../../common/call-stack-parser" -import {processTxString, TestDataMessage, TestRun} from "./test-types" +import {processTxString, TestDataMessage, TransactionRun} from "./test-types" interface TestTreeItem { readonly id: string @@ -23,25 +23,24 @@ export class TestTreeProvider implements vscode.TreeDataProvider { public readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event - private readonly testRunsByName: Map = new Map() + private readonly txRunsByName: Map = new Map() public addTestData(data: TestDataMessage): void { const transactions = processTxString(data.transactions) - const testRun: TestRun = { + const txRun: TransactionRun = { id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, name: data.testName, timestamp: Date.now(), transactions: transactions, contracts: data.contracts, - changes: data.changes, resultString: data.transactions, } - const existingRuns = this.testRunsByName.get(data.testName) ?? [] - existingRuns.push(testRun) + const existingRuns = this.txRunsByName.get(data.testName) ?? [] + existingRuns.push(txRun) - this.testRunsByName.set(data.testName, existingRuns) + this.txRunsByName.set(data.testName, existingRuns) this._onDidChangeTreeData.fire(undefined) } @@ -61,10 +60,10 @@ export class TestTreeProvider implements vscode.TreeDataProvider { public getChildren(element?: TestTreeItem): Thenable { if (!element) { return Promise.resolve( - [...this.testRunsByName.keys()].map(testName => ({ + [...this.txRunsByName.keys()].map(testName => ({ id: `test-group-${testName}`, label: testName, - description: `${this.testRunsByName.get(testName)?.length ?? 0} transactions`, + description: `${this.txRunsByName.get(testName)?.length ?? 0} transactions`, contextValue: "testGroup", iconPath: new vscode.ThemeIcon("beaker"), collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, @@ -74,11 +73,11 @@ export class TestTreeProvider implements vscode.TreeDataProvider { } if (element.type === "testName") { - const testRuns = this.testRunsByName.get(element.label) ?? [] + const txRuns = this.txRunsByName.get(element.label) ?? [] return Promise.all( - testRuns.map(async (testRun, index) => { + txRuns.map(async (testRun, index) => { const extractedName = await extractTransactionName(testRun) - const exitCode = getTestRunExitCode(testRun) + const exitCode = getTxRunExitCode(testRun) const baseLabel = extractedName ?? `Transaction #${index}` const label = exitCode === 0 ? baseLabel : `${baseLabel} (exit: ${exitCode})` @@ -111,40 +110,15 @@ export class TestTreeProvider implements vscode.TreeDataProvider { return Promise.resolve([]) } - - public getTestRun(testRunId: string): TestRun | undefined { - for (const testRuns of this.testRunsByName.values()) { - const found = testRuns.find(run => run.id === testRunId) - if (found) return found - } - return undefined - } - - public clearAllTests(): void { - this.testRunsByName.clear() - this._onDidChangeTreeData.fire(undefined) - } - - public removeTestRun(testRunId: string): void { - for (const [testName, testRuns] of this.testRunsByName.entries()) { - const filteredRuns = testRuns.filter(run => run.id !== testRunId) - if (filteredRuns.length === 0) { - this.testRunsByName.delete(testName) - } else { - this.testRunsByName.set(testName, filteredRuns) - } - } - this._onDidChangeTreeData.fire(undefined) - } } -async function extractTransactionName(testRun: TestRun): Promise { - const transactionWithCallStack = testRun.transactions.find(tx => tx.callStack) - if (!transactionWithCallStack?.callStack) { +async function extractTransactionName(txRun: TransactionRun): Promise { + const txWithCallStack = txRun.transactions.find(tx => tx.callStack) + if (!txWithCallStack?.callStack) { return undefined } - const parsedCallStack = parseCallStack(transactionWithCallStack.callStack) + const parsedCallStack = parseCallStack(txWithCallStack.callStack) if (parsedCallStack.length === 0) { return undefined } @@ -209,8 +183,8 @@ async function extractTransactionName(testRun: TestRun): Promise { +function getTxRunExitCode(txRun: TransactionRun): number { + const failedTransaction = txRun.transactions.find(tx => { if (tx.computeInfo === "skipped") return false const exitCode = tx.computeInfo.exitCode return exitCode !== 0 && exitCode !== 1 diff --git a/editors/code/src/providers/sandbox/test-types.ts b/editors/code/src/providers/sandbox/test-types.ts index aa34222a..459e35e0 100644 --- a/editors/code/src/providers/sandbox/test-types.ts +++ b/editors/code/src/providers/sandbox/test-types.ts @@ -24,29 +24,20 @@ export interface ContractData { readonly meta?: ContractMeta } -export interface StateChange { - readonly address: string - readonly lt: string - readonly before: string - readonly after: string -} - export interface TestDataMessage { readonly $: "test-data" readonly testName: string readonly transactions: string readonly contracts: readonly ContractData[] - readonly changes: readonly StateChange[] } -export interface TestRun { +export interface TransactionRun { readonly id: string readonly name: string readonly timestamp: number readonly resultString: string readonly transactions: readonly TransactionInfo[] readonly contracts: readonly ContractData[] - readonly changes: readonly StateChange[] } function parseTransactions(data: string): RawTransactions | undefined { From a14f8e23411e0bbabdc72f92502ca1eee9094fd5 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:14:59 +0400 Subject: [PATCH 10/16] simplify --- .../code/src/common/types/raw-transaction.ts | 25 ++++++++++++++ .../src/providers/sandbox/TestTreeProvider.ts | 5 +-- .../code/src/providers/sandbox/test-types.ts | 32 +----------------- .../src/views/details/TransactionDetails.tsx | 33 ++----------------- 4 files changed, 31 insertions(+), 64 deletions(-) diff --git a/editors/code/src/common/types/raw-transaction.ts b/editors/code/src/common/types/raw-transaction.ts index bf352760..9c697ba6 100644 --- a/editors/code/src/common/types/raw-transaction.ts +++ b/editors/code/src/common/types/raw-transaction.ts @@ -293,3 +293,28 @@ const computeFinalData = ( export const processRawTransactions = (txs: RawTransactionInfo[]): TransactionInfo[] => { return txs.map(tx => processRawTx(tx, txs, new Map())) } + +function parseTransactions(data: string): RawTransactions | undefined { + try { + return JSON.parse(data) as RawTransactions + } catch { + return undefined + } +} + +export function processTxString(resultString: string): TransactionInfo[] | undefined { + const rawTxs = parseTransactions(resultString) + if (!rawTxs) { + return undefined + } + + const parsedTransactions = rawTxs.transactions.map( + (it): RawTransactionInfo => ({ + ...it, + transaction: it.transaction, + parsedTransaction: loadTransaction(Cell.fromHex(it.transaction).asSlice()), + }), + ) + + return processRawTransactions(parsedTransactions) +} diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index 819ccfdf..94d58082 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -3,8 +3,9 @@ import * as vscode from "vscode" import {parseCallStack} from "../../common/call-stack-parser" +import {processTxString} from "../../common/types/raw-transaction" -import {processTxString, TestDataMessage, TransactionRun} from "./test-types" +import {TestDataMessage, TransactionRun} from "./test-types" interface TestTreeItem { readonly id: string @@ -32,7 +33,7 @@ export class TestTreeProvider implements vscode.TreeDataProvider { id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, name: data.testName, timestamp: Date.now(), - transactions: transactions, + transactions: transactions ?? [], contracts: data.contracts, resultString: data.transactions, } diff --git a/editors/code/src/providers/sandbox/test-types.ts b/editors/code/src/providers/sandbox/test-types.ts index 459e35e0..00780f9c 100644 --- a/editors/code/src/providers/sandbox/test-types.ts +++ b/editors/code/src/providers/sandbox/test-types.ts @@ -1,14 +1,9 @@ // SPDX-License-Identifier: MIT // Copyright © 2025 TON Core -import {Cell, ContractABI as BadContractABI, loadTransaction} from "@ton/core" +import {ContractABI as BadContractABI} from "@ton/core" import {TransactionInfo} from "../../common/types/transaction" -import { - processRawTransactions, - RawTransactionInfo, - RawTransactions, -} from "../../common/types/raw-transaction" import {HexString} from "../../common/hex-string" export interface ContractMeta { @@ -39,28 +34,3 @@ export interface TransactionRun { readonly transactions: readonly TransactionInfo[] readonly contracts: readonly ContractData[] } - -function parseTransactions(data: string): RawTransactions | undefined { - try { - return JSON.parse(data) as RawTransactions - } catch { - return undefined - } -} - -export function processTxString(resultString: string): TransactionInfo[] { - const rawTxs = parseTransactions(resultString) - if (!rawTxs) { - return [] - } - - const parsedTransactions = rawTxs.transactions.map( - (it): RawTransactionInfo => ({ - ...it, - transaction: it.transaction, - parsedTransaction: loadTransaction(Cell.fromHex(it.transaction).asSlice()), - }), - ) - - return processRawTransactions(parsedTransactions) -} diff --git a/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx b/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx index 7c22de3f..2cdef5fb 100644 --- a/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx +++ b/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx @@ -1,13 +1,9 @@ import React, {JSX, useEffect, useMemo, useState} from "react" -import {Address, Cell, loadShardAccount, loadTransaction} from "@ton/core" +import {Address, Cell, loadShardAccount} from "@ton/core" import {TransactionDetailsInfo, TransactionInfo} from "../../../../common/types/transaction" -import { - processRawTransactions, - RawTransactionInfo, - RawTransactions, -} from "../../../../common/types/raw-transaction" +import {processTxString} from "../../../../common/types/raw-transaction" import {ContractData} from "../../../../common/types/contract" import {LoadingSpinner} from "../../components/common" @@ -138,28 +134,3 @@ export default function TransactionDetails({vscode}: Props): JSX.Element { ) } - -function parseTransactions(data: string): RawTransactions | undefined { - try { - return JSON.parse(data) as RawTransactions - } catch { - return undefined - } -} - -function processTxString(resultString: string): TransactionInfo[] | undefined { - const rawTxs = parseTransactions(resultString) - if (!rawTxs) { - return undefined - } - - const parsedTransactions = rawTxs.transactions.map( - (it): RawTransactionInfo => ({ - ...it, - transaction: it.transaction, - parsedTransaction: loadTransaction(Cell.fromHex(it.transaction).asSlice()), - }), - ) - - return processRawTransactions(parsedTransactions) -} From 7f25e694e4dcb0edbc1c0a1600ad9d9a86947d94 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:22:11 +0400 Subject: [PATCH 11/16] less diff --- .../code/src/providers/sandbox/WebSocketServer.ts | 2 -- .../src/views/actions/sandbox-actions-types.ts | 8 -------- .../TransactionTree/TransactionTree.module.css | 7 ------- .../components/TransactionTree/TransactionTree.tsx | 13 +++---------- package.json | 4 ++++ server/src/server.ts | 1 + 6 files changed, 8 insertions(+), 27 deletions(-) diff --git a/editors/code/src/providers/sandbox/WebSocketServer.ts b/editors/code/src/providers/sandbox/WebSocketServer.ts index eebf9ee2..a5cdb233 100644 --- a/editors/code/src/providers/sandbox/WebSocketServer.ts +++ b/editors/code/src/providers/sandbox/WebSocketServer.ts @@ -18,8 +18,6 @@ export class WebSocketServer { this.wss = new Server({port: this.port}) this.wss.on("connection", (ws: WebSocket) => { - console.log("Blockchain WebSocket connected") - ws.on("message", (data: Buffer) => { try { const message = JSON.parse(data.toString()) as TestDataMessage diff --git a/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts b/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts index aed6c347..22a03a20 100644 --- a/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts +++ b/editors/code/src/webview-ui/src/views/actions/sandbox-actions-types.ts @@ -242,13 +242,6 @@ export interface UpdateConnectionStatusMessage { readonly isConnected: boolean } -export interface OpenFileAtPositionMessage { - readonly type: "openFileAtPosition" - readonly uri: string - readonly row: number - readonly column: number -} - export type VSCodeMessage = | UpdateContractsMessage | ShowResultMessage @@ -264,7 +257,6 @@ export type VSCodeMessage = | TemplateUpdatedMessage | TemplateDeletedMessage | UpdateConnectionStatusMessage - | OpenFileAtPositionMessage export type VSCodeCommand = | SendExternalMessageCommand diff --git a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css index b3cb573b..a9d6c946 100644 --- a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css +++ b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.module.css @@ -117,7 +117,6 @@ display: flex; flex-direction: column; gap: var(--spacing-md); - position: relative; } .tooltipField { @@ -171,12 +170,6 @@ gap: 2px; } -.nodeHeader { - display: flex; - align-items: center; - gap: var(--spacing-xs); -} - .bottonText { display: flex; flex-direction: column; diff --git a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx index 47ff0324..37204fa2 100644 --- a/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx +++ b/editors/code/src/webview-ui/src/views/details/components/TransactionTree/TransactionTree.tsx @@ -8,13 +8,12 @@ import {TransactionDetails} from "../index" import {formatCurrency} from "../../../../components/format/format" import {ContractData} from "../../../../../../common/types/contract" import {TransactionInfo} from "../../../../../../common/types/transaction" +import {VSCodeTransactionDetailsAPI} from "../../transaction-details-types" import {ParsedDataView} from "../ParsedDataView/ParsedDataView" import {parseData, ParsedObject} from "../../../../../../common/binary" -import {VSCodeTransactionDetailsAPI} from "../../transaction-details-types" - import {useTooltip} from "./useTooltip" import {SmartTooltip} from "./SmartTooltip" @@ -70,13 +69,7 @@ const formatAddressShort = (address: Address | undefined): string => { return addressStr.slice(0, 6) + "..." + addressStr.slice(-6) } -function TransactionTooltipContent({ - data, - vscode, -}: { - data: TransactionTooltipData - vscode: VSCodeTransactionDetailsAPI -}): React.JSX.Element { +function TransactionTooltipContent({data}: {data: TransactionTooltipData}): React.JSX.Element { return (
@@ -279,7 +272,7 @@ export function TransactionTree({ showTooltip({ x: rect.left, y: rect.top, - content: , + content: , }) } diff --git a/package.json b/package.json index 40256642..6b951085 100644 --- a/package.json +++ b/package.json @@ -454,6 +454,10 @@ { "command": "ton.sandbox.copyContractAddressFromTree", "when": "false" + }, + { + "command": "ton.test.openTestSource", + "when": "false" } ], "explorer/context": [ diff --git a/server/src/server.ts b/server/src/server.ts index dc72b507..68409041 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -5,6 +5,7 @@ import * as path from "node:path" import {fileURLToPath} from "node:url" import * as lsp from "vscode-languageserver" + import {DidChangeWatchedFilesParams, FileChangeType, RenameFilesParams} from "vscode-languageserver" import {WorkspaceEdit} from "vscode-languageserver-types" From 049f1c51ab3f2b069347f9b3a07bb6e5b9d63e4f Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:48:18 +0400 Subject: [PATCH 12/16] simplify show transaction details command --- editors/code/src/commands/sandboxCommands.ts | 50 +++---------- editors/code/src/common/types/contract.ts | 4 +- .../code/src/common/types/raw-transaction.ts | 4 +- editors/code/src/common/types/transaction.ts | 18 +---- editors/code/src/extension.ts | 70 +++++-------------- .../src/providers/sandbox/TestTreeProvider.ts | 4 +- .../code/src/providers/sandbox/test-types.ts | 2 +- .../src/views/actions/ActionsApp.tsx | 6 +- .../src/views/details/TransactionDetails.tsx | 34 ++------- 9 files changed, 38 insertions(+), 154 deletions(-) diff --git a/editors/code/src/commands/sandboxCommands.ts b/editors/code/src/commands/sandboxCommands.ts index c77776bb..b2792fba 100644 --- a/editors/code/src/commands/sandboxCommands.ts +++ b/editors/code/src/commands/sandboxCommands.ts @@ -2,8 +2,6 @@ // Copyright © 2025 TON Studio import vscode from "vscode" -import {ContractAbi} from "@shared/abi" - import {SandboxTreeProvider} from "../providers/sandbox/SandboxTreeProvider" import {SandboxActionsProvider, TransactionInfo} from "../providers/sandbox/SandboxActionsProvider" import {HistoryWebviewProvider} from "../providers/sandbox/HistoryWebviewProvider" @@ -14,7 +12,6 @@ import { deleteMessageTemplate, exportTrace, importTrace, - loadContractInfo, loadLatestOperationResult, OperationTrace, redeployContract, @@ -24,8 +21,6 @@ import { ShowTransactionDetailsCommand, } from "../webview-ui/src/views/actions/sandbox-actions-types" import {TransactionDetailsProvider} from "../providers/sandbox/TransactionDetailsProvider" -import {HexString} from "../common/hex-string" -import {Base64String} from "../common/base64-string" import {TransactionDetailsInfo} from "../common/types/transaction" import { detectPackageManager, @@ -428,35 +423,18 @@ export function registerSandboxCommands( const deployedContracts = treeProvider.getDeployedContracts() - let account: HexString | undefined - let stateInit: {code: Base64String; data: Base64String} | undefined - let abi: ContractAbi | undefined - let resultString = args.resultString - - try { - const contractInfo = await loadContractInfo(args.contractAddress) - if (contractInfo.success) { - account = contractInfo.data.account - stateInit = contractInfo.data.stateInit - abi = contractInfo.data.abi - } - } catch (error) { - vscode.window.showErrorMessage( - `Failed to fetch contract info from server: ${error}`, - ) - console.warn("Failed to fetch contract info from server:", error) - } - - if (!resultString) { + let serializedResult = args.serializedResult + if (!serializedResult) { + // For the "Transaction Details" button we need to load data, since we don't have it in the webview try { const latestOperationResult = await loadLatestOperationResult() - if (latestOperationResult.success) { - resultString = latestOperationResult.data.resultString - } else { - const message = `Failed to load latest operation result: ${latestOperationResult.error}` - vscode.window.showErrorMessage(message) - console.warn(message) + if (!latestOperationResult.success) { + throw new Error( + `Failed to load latest operation result: ${latestOperationResult.error}`, + ) } + + serializedResult = latestOperationResult.data.resultString } catch (error) { vscode.window.showErrorMessage( `Failed to fetch latest operation result from daemon: ${error}`, @@ -466,16 +444,8 @@ export function registerSandboxCommands( } const transaction: TransactionDetailsInfo = { - contractAddress: args.contractAddress, - methodName: args.methodName, - transactionId: args.transactionId, - timestamp: args.timestamp, - status: args.status, - resultString, + serializedResult, deployedContracts, - account, - stateInit, - abi, } transactionDetailsProvider.showTransactionDetails(transaction) diff --git a/editors/code/src/common/types/contract.ts b/editors/code/src/common/types/contract.ts index 447a2f6b..e0af8e25 100644 --- a/editors/code/src/common/types/contract.ts +++ b/editors/code/src/common/types/contract.ts @@ -1,4 +1,4 @@ -import {Address, ShardAccount, StateInit} from "@ton/core" +import {Address} from "@ton/core" import {SourceMap} from "ton-source-map" @@ -6,8 +6,6 @@ import {ContractAbi} from "@shared/abi" export interface ContractData { readonly address: Address - readonly stateInit: StateInit | undefined - readonly account: ShardAccount readonly letter: string readonly displayName: string readonly kind: "treasury" | "user-contract" diff --git a/editors/code/src/common/types/raw-transaction.ts b/editors/code/src/common/types/raw-transaction.ts index 9c697ba6..86321b17 100644 --- a/editors/code/src/common/types/raw-transaction.ts +++ b/editors/code/src/common/types/raw-transaction.ts @@ -302,8 +302,8 @@ function parseTransactions(data: string): RawTransactions | undefined { } } -export function processTxString(resultString: string): TransactionInfo[] | undefined { - const rawTxs = parseTransactions(resultString) +export function processTxString(serializedResult: string): TransactionInfo[] | undefined { + const rawTxs = parseTransactions(serializedResult) if (!rawTxs) { return undefined } diff --git a/editors/code/src/common/types/transaction.ts b/editors/code/src/common/types/transaction.ts index b83e99e8..fcf785a0 100644 --- a/editors/code/src/common/types/transaction.ts +++ b/editors/code/src/common/types/transaction.ts @@ -1,27 +1,11 @@ import {type Address, Cell, type OutAction, type Transaction} from "@ton/core" import {SourceMap} from "ton-source-map" -import {ContractAbi} from "@shared/abi" - -import {HexString} from "../hex-string" -import {Base64String} from "../base64-string" - import {DeployedContract} from "./contract" export interface TransactionDetailsInfo { - readonly contractAddress: string - readonly methodName: string - readonly transactionId?: string - readonly timestamp: string - readonly status: "success" | "pending" | "failed" - readonly resultString?: string + readonly serializedResult?: string readonly deployedContracts?: readonly DeployedContract[] - readonly account?: HexString - readonly stateInit?: { - readonly code: Base64String - readonly data: Base64String - } - readonly abi?: ContractAbi } /** diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 3d4bf953..82173219 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -13,8 +13,6 @@ import { TransportKind, } from "vscode-languageclient/node" -import {Cell, loadStateInit} from "@ton/core" - import { DocumentationAtPositionRequest, GetWorkspaceContractsAbiResponse, @@ -56,7 +54,6 @@ import {configureDebugging} from "./debugging" import {ContractData, TransactionRun} from "./providers/sandbox/test-types" import {TransactionDetailsInfo} from "./common/types/transaction" import {DeployedContract} from "./common/types/contract" -import {Base64String} from "./common/base64-string" let client: LanguageClient | undefined = undefined let cachedToolchainInfo: SetToolchainVersionParams | undefined = undefined @@ -126,57 +123,24 @@ export async function activate(context: vscode.ExtensionContext): Promise "tolk.getWorkspaceContractsAbi", ) - // TODO: redone - const transaction = txRun.transactions.at(1) ?? txRun.transactions.at(0) - if (transaction) { - const contract = txRun.contracts.find( - it => it.address === transaction.address?.toString(), - ) - - const stateInit = loadStateInit( - Cell.fromHex(contract?.stateInit ?? "").asSlice(), - ) - - const workspaceContract = workspaceContracts.contracts.find(it => - sameNameContract(it, contract), - ) - const abi = workspaceContract?.abi - - const transactionDetailsInfo: TransactionDetailsInfo = { - contractAddress: transaction.address?.toString() ?? "unknown", - methodName: "test-transaction", - transactionId: transaction.transaction.lt.toString(), - timestamp: new Date().toISOString(), - status: "success" as const, // For now, assume success - resultString: txRun.resultString, - deployedContracts: txRun.contracts.map((contract): DeployedContract => { - const workspaceContract = workspaceContracts.contracts.find(it => - sameNameContract(it, contract), - ) - return { - abi: workspaceContract?.abi, - sourceMap: undefined, - address: contract.address, - deployTime: undefined, - name: contract.meta?.wrapperName ?? "unknown", - sourceUri: "", - } - }), - account: contract?.account, - stateInit: { - code: (stateInit.code ?? new Cell()) - .toBoc() - .toString("base64") as Base64String, - data: (stateInit.data ?? new Cell()) - .toBoc() - .toString("base64") as Base64String, - }, - abi, - } - - console.log("transactionDetailsInfo", transactionDetailsInfo) - transactionDetailsProvider.showTransactionDetails(transactionDetailsInfo) + const transactionDetailsInfo: TransactionDetailsInfo = { + serializedResult: txRun.serializedResult, + deployedContracts: txRun.contracts.map((contract): DeployedContract => { + const workspaceContract = workspaceContracts.contracts.find(it => + sameNameContract(it, contract), + ) + return { + abi: workspaceContract?.abi, + sourceMap: undefined, + address: contract.address, + deployTime: undefined, + name: contract.meta?.wrapperName ?? "unknown", + sourceUri: workspaceContract?.path ?? "", + } + }), } + + transactionDetailsProvider.showTransactionDetails(transactionDetailsInfo) }, ), vscode.commands.registerCommand( diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index 94d58082..7101d77b 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -35,7 +35,7 @@ export class TestTreeProvider implements vscode.TreeDataProvider { timestamp: Date.now(), transactions: transactions ?? [], contracts: data.contracts, - resultString: data.transactions, + serializedResult: data.transactions, } const existingRuns = this.txRunsByName.get(data.testName) ?? [] @@ -153,11 +153,9 @@ async function extractTransactionName(txRun: TransactionRun): Promise { + // serializedResult and deployedContracts will be loaded on demand vscode.postMessage({ type: "showTransactionDetails", - contractAddress: tx.contractAddress, - methodName: tx.methodName, - transactionId: tx.transactionId, - timestamp: tx.timestamp, - status: "success", }) }} result={ diff --git a/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx b/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx index 2cdef5fb..53a34240 100644 --- a/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx +++ b/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx @@ -1,6 +1,6 @@ import React, {JSX, useEffect, useMemo, useState} from "react" -import {Address, Cell, loadShardAccount} from "@ton/core" +import {Address} from "@ton/core" import {TransactionDetailsInfo, TransactionInfo} from "../../../../common/types/transaction" import {processTxString} from "../../../../common/types/raw-transaction" @@ -32,33 +32,10 @@ export default function TransactionDetails({vscode}: Props): JSX.Element { const [transaction, setTransaction] = useState(undefined) const [transactions, setTransactions] = useState(undefined) - const parsedAccount = useMemo(() => { - if (!transaction?.account) return null - try { - return loadShardAccount(Cell.fromHex(transaction.account).asSlice()) - } catch (error) { - console.warn("Failed to parse account data:", error) - return null - } - }, [transaction?.account]) - - const parsedStateInit = useMemo(() => { - if (!transaction?.stateInit?.code || !transaction.stateInit.data) return undefined - try { - return { - code: Cell.fromBase64(transaction.stateInit.code), - data: Cell.fromBase64(transaction.stateInit.data), - } - } catch (error) { - console.warn("Failed to parse stateInit data:", error) - return undefined - } - }, [transaction?.stateInit?.code, transaction?.stateInit?.data]) - useMemo(() => { - if (!transaction || !transaction.resultString) return + if (!transaction || !transaction.serializedResult) return - const transactionInfos = processTxString(transaction.resultString) + const transactionInfos = processTxString(transaction.serializedResult) if (!transactionInfos) { return } @@ -108,19 +85,16 @@ export default function TransactionDetails({vscode}: Props): JSX.Element { if (!transaction.deployedContracts) return [] return transaction.deployedContracts.flatMap((it, index) => { - if (!parsedAccount) return [] const letter = String.fromCodePoint(65 + (index % 26)) return { displayName: it.name, address: Address.parse(it.address), kind: it.name === "treasury" ? "treasury" : "user-contract", letter, - stateInit: parsedStateInit, - account: parsedAccount, abi: it.abi, } satisfies ContractData }) - }, [transaction, parsedAccount, parsedStateInit]) + }, [transaction]) if (!transaction) { return From 2e7732487a0871cec0e637db19e691d35a8e662e Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:49:21 +0400 Subject: [PATCH 13/16] more rename --- editors/code/src/commands/sandboxCommands.ts | 4 ++-- .../src/providers/sandbox/TransactionDetailsProvider.ts | 4 ++-- .../webview-ui/src/views/details/TransactionDetails.tsx | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/editors/code/src/commands/sandboxCommands.ts b/editors/code/src/commands/sandboxCommands.ts index b2792fba..e4c73ecd 100644 --- a/editors/code/src/commands/sandboxCommands.ts +++ b/editors/code/src/commands/sandboxCommands.ts @@ -453,8 +453,8 @@ export function registerSandboxCommands( ), vscode.commands.registerCommand( "ton.sandbox.addTransactionsToDetails", - (resultString: string): void => { - transactionDetailsProvider.addTransactions(resultString) + (serializedResult: string): void => { + transactionDetailsProvider.addTransactions(serializedResult) }, ), ) diff --git a/editors/code/src/providers/sandbox/TransactionDetailsProvider.ts b/editors/code/src/providers/sandbox/TransactionDetailsProvider.ts index f535be7c..257a9a1b 100644 --- a/editors/code/src/providers/sandbox/TransactionDetailsProvider.ts +++ b/editors/code/src/providers/sandbox/TransactionDetailsProvider.ts @@ -68,11 +68,11 @@ export class TransactionDetailsProvider { } } - public addTransactions(resultString: string): void { + public addTransactions(serializedResult: string): void { if (this.panel) { void this.panel.webview.postMessage({ type: "addTransactions", - resultString, + serializedResult, }) } } diff --git a/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx b/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx index 53a34240..33972364 100644 --- a/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx +++ b/editors/code/src/webview-ui/src/views/details/TransactionDetails.tsx @@ -21,7 +21,7 @@ interface Message { interface AddTransactionsMessage { readonly type: "addTransactions" - readonly resultString: string + readonly serializedResult: string } interface Props { @@ -42,8 +42,8 @@ export default function TransactionDetails({vscode}: Props): JSX.Element { setTransactions(transactionInfos) }, [transaction, setTransactions]) - const addTransactions = (resultString: string): void => { - const newTransactionInfos = processTxString(resultString) + const addTransactions = (serializedResult: string): void => { + const newTransactionInfos = processTxString(serializedResult) if (!newTransactionInfos) { return } @@ -70,7 +70,7 @@ export default function TransactionDetails({vscode}: Props): JSX.Element { if (message.type === "updateTransactionDetails") { setTransaction(message.transaction) } else { - addTransactions(message.resultString) + addTransactions(message.serializedResult) } } From f518c5b2b898a91323cbe9c354900bf75d95a12c Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:59:29 +0400 Subject: [PATCH 14/16] refactor --- editors/code/src/extension.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index 82173219..a5f3b66c 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -58,16 +58,6 @@ import {DeployedContract} from "./common/types/contract" let client: LanguageClient | undefined = undefined let cachedToolchainInfo: SetToolchainVersionParams | undefined = undefined -function sameNameContract(it: WorkspaceContractInfo, contract: ContractData | undefined): boolean { - const leftName = it.name - const rightName = contract?.meta?.wrapperName ?? "" - - const normalizedLeft = leftName.toLowerCase().replace("-contract", "").replace(/-/g, "") - const normalizedRight = rightName.toLowerCase().replace("-contract", "").replace(/-/g, "") - - return normalizedLeft === normalizedRight -} - export async function activate(context: vscode.ExtensionContext): Promise { await checkConflictingExtensions() @@ -118,6 +108,19 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.commands.registerCommand( "ton.test.showTransactionDetails", async (txRun: TransactionRun) => { + const normalizeName = (name: string): string => { + return name.toLowerCase().replace("-contract", "").replace(/[_-]/g, "") + } + // a bit hacky :) + const sameNameContract = ( + workspaceContract: WorkspaceContractInfo, + contract: ContractData | undefined, + ): boolean => { + const leftName = workspaceContract.name + const rightName = contract?.meta?.wrapperName ?? "" + return normalizeName(leftName) === normalizeName(rightName) + } + const workspaceContracts = await vscode.commands.executeCommand( "tolk.getWorkspaceContractsAbi", @@ -126,8 +129,8 @@ export async function activate(context: vscode.ExtensionContext): Promise const transactionDetailsInfo: TransactionDetailsInfo = { serializedResult: txRun.serializedResult, deployedContracts: txRun.contracts.map((contract): DeployedContract => { - const workspaceContract = workspaceContracts.contracts.find(it => - sameNameContract(it, contract), + const workspaceContract = workspaceContracts.contracts.find( + workspaceContract => sameNameContract(workspaceContract, contract), ) return { abi: workspaceContract?.abi, From b897d0d62beea85548787a320055395028543949 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:05:26 +0400 Subject: [PATCH 15/16] fix --- .../src/providers/sandbox/TestTreeProvider.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/editors/code/src/providers/sandbox/TestTreeProvider.ts b/editors/code/src/providers/sandbox/TestTreeProvider.ts index 7101d77b..e868796f 100644 --- a/editors/code/src/providers/sandbox/TestTreeProvider.ts +++ b/editors/code/src/providers/sandbox/TestTreeProvider.ts @@ -27,21 +27,27 @@ export class TestTreeProvider implements vscode.TreeDataProvider { private readonly txRunsByName: Map = new Map() public addTestData(data: TestDataMessage): void { - const transactions = processTxString(data.transactions) + const transactions = processTxString(data.transactions) ?? [] const txRun: TransactionRun = { id: `test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, name: data.testName, timestamp: Date.now(), - transactions: transactions ?? [], + transactions: transactions, contracts: data.contracts, serializedResult: data.transactions, } const existingRuns = this.txRunsByName.get(data.testName) ?? [] - existingRuns.push(txRun) - - this.txRunsByName.set(data.testName, existingRuns) + const lastRun = existingRuns.at(-1) + + // Consider all transactions as a new test run after 20 seconds + if (lastRun && Date.now() - lastRun.timestamp > 20 * 1000) { + this.txRunsByName.set(data.testName, [txRun]) + } else { + existingRuns.push(txRun) + this.txRunsByName.set(data.testName, existingRuns) + } this._onDidChangeTreeData.fire(undefined) } From f2adb312ce3f5a2cf9249854e6cb9141ea128972 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:23:32 +0400 Subject: [PATCH 16/16] use custom icon --- editors/code/src/assets/icons/test-icon.svg | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 editors/code/src/assets/icons/test-icon.svg diff --git a/editors/code/src/assets/icons/test-icon.svg b/editors/code/src/assets/icons/test-icon.svg new file mode 100644 index 00000000..1062fa0f --- /dev/null +++ b/editors/code/src/assets/icons/test-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/package.json b/package.json index 6b951085..f212b709 100644 --- a/package.json +++ b/package.json @@ -428,7 +428,7 @@ { "id": "tonTestContainer", "title": "TON Tests", - "icon": "$(beaker)" + "icon": "./dist/icons/test-icon.svg" } ] },