├── .eslintrc.json ├── .gitignore ├── .vscode └── launch.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── resources ├── howtorun.png ├── printf_demo.gif ├── step_back_demo.gif └── vt.png ├── src ├── codeLang │ ├── index.ts │ └── python │ │ ├── deserialize.ts │ │ ├── index.ts │ │ └── variableValue.ts ├── core │ ├── entity │ │ ├── codeLang.ts │ │ ├── logLoader.ts │ │ ├── scope.ts │ │ └── value.ts │ ├── feature │ │ ├── lineSteps.ts │ │ ├── logMetadata.ts │ │ ├── srcFiles.ts │ │ ├── stepInfo.ts │ │ ├── stepVars.ts │ │ └── varChangeLog.ts │ ├── index.ts │ └── lib │ │ ├── abbrivate.ts │ │ ├── cache.ts │ │ ├── decorateDiff.ts │ │ ├── paging.ts │ │ └── search.ts ├── dumper │ ├── create_tracelog_table.sql │ ├── lib.ts │ └── python │ │ ├── config.py │ │ ├── dump_handler.py │ │ ├── main.py │ │ ├── serializer.py │ │ └── sqlite_saver.py ├── logLoader │ └── sqliteLogLoader.ts ├── util │ └── utilityTypes.ts └── vscodeExtension │ ├── extension │ ├── extension.ts │ ├── init.ts │ ├── messageHandler.ts │ ├── processors │ │ ├── breakPoint.ts │ │ ├── common.ts │ │ ├── dumpConf.ts │ │ ├── logFile.ts │ │ ├── panelHandle.ts │ │ ├── step.ts │ │ ├── valueShowOption.ts │ │ └── varChangeLog.ts │ ├── store │ │ ├── proc.ts │ │ ├── state.ts │ │ └── store.ts │ └── uiWrapper │ │ ├── editorPanel.ts │ │ ├── fileSystemPicker.ts │ │ ├── notification.ts │ │ ├── terminal.ts │ │ ├── webviewPanel.ts │ │ └── webviewView.ts │ ├── messaging.ts │ └── webview │ ├── accessor.ts │ ├── components │ ├── BreakPointSteps.tsx │ ├── CircularBackdrop.tsx │ ├── HighlightedTree.tsx │ ├── SearchBar.tsx │ ├── SelectList.tsx │ ├── StepLink.tsx │ └── ValueShowSettings.tsx │ ├── index.tsx │ ├── panel │ ├── StepDetailPanel.tsx │ └── VarChangeLogPanel.tsx │ ├── sidebar │ ├── DumpConf.tsx │ ├── PanelControlls.tsx │ ├── Sidebar.tsx │ └── Status.tsx │ ├── webviewMessaging.tsx │ └── webviewTheme.ts ├── test ├── core.test.ts ├── dump.test.ts ├── dump_conf │ ├── data_types.vt_dump_conf │ └── httpie.vt_dump_conf └── target_files │ └── python │ ├── assert.py │ ├── data_types.py │ ├── demo.py │ ├── exception.py │ ├── exit.py │ ├── func.py │ ├── huge_loop.py │ ├── multibyte_string.py │ ├── small.py │ └── small_loop.py ├── tsconfig.extension.json ├── tsconfig.json ├── tsconfig.webview.json ├── vsc-extension-quickstart.md └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | build/ 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vscode 34 | dist/ 35 | tmp/ 36 | *.vsix 37 | 38 | # vartrace sqlite log 39 | *.db 40 | *.db-shm 41 | *.db-wal 42 | 43 | # python cache 44 | __pycache__ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // IntelliSense を使用して利用可能な属性を学べます。 3 | // 既存の属性の説明をホバーして表示します。 4 | // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | }, 15 | { 16 | "type": "extensionHost", 17 | "request": "launch", 18 | "name": "拡張機能の起動", 19 | "runtimeExecutable": "${execPath}", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}/" 22 | ], 23 | "outFiles": [ 24 | "${workspaceFolder}/dist/**/*.js" 25 | ], 26 | //"preLaunchTask": "yarn watch" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | test/** 14 | .vartrace/** 15 | node_modules 16 | !node_modules/better-sqlite3 17 | !node_modules/pg 18 | !node_modules/mysql2 19 | !node_modules/bindings 20 | !node_modules/file-uri-to-path -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | - v0.0.1: 4 | - initial release 5 | - v0.0.2: 6 | - support regular expression search 7 | - add python3.8 support 8 | - avoid pycache conflict 9 | - fix wrong complement of execution command -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VarTrace 2 | VarTrace is a python debugger that records & analyze your script executions 3 | 4 | ## Features 5 | 6 | * printf debug without modifing your code 7 | * search & filter by variable name or value 8 | ![printf](https://github.com/zat-dev/VarTrace/raw/main/resources/printf_demo.gif) 9 | * step back, jump 10 | ![jump](https://github.com/zat-dev/VarTrace/raw/main/resources/step_back_demo.gif) 11 | 12 | ## quick start 13 | 0. search and install "vartrace" vscode extension. then VT icon will appear the left of vscode 14 | 1. click VT icon and fill your execution command into sidebar textfield. the command is same as is you run your script 15 | * for example, `python demo.py -a arg1` 16 | 2. push analyze button 17 | 3. do the above Features section 18 | 19 | ![howto](https://github.com/zat-dev/VarTrace/raw/main/resources/howtorun.png) 20 | 21 | ## current support 22 | 23 | * language : python 24 | * editor: vscode 25 | * single thread only 26 | * platform: windows 27 | 28 | # limitation 29 | * heavy performance degradation 30 | 31 | ## Extension Settings 32 | * fill your python execution commands and push analyze button 33 | 34 | # LICENSE 35 | * GPL v3 36 | 37 | 38 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel used for only jest 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', { targets: { node: 'current' } }], 5 | '@babel/preset-typescript', 6 | ], 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vartrace", 3 | "publisher": "zat", 4 | "displayName": "vartrace", 5 | "description": "debug and analyzer", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/zat-dev/VarTrace.git" 9 | }, 10 | "version": "0.0.2", 11 | "engines": { 12 | "vscode": "^1.61.0" 13 | }, 14 | "categories": [ 15 | "Other" 16 | ], 17 | "activationEvents": [ 18 | "onCommand:extension.vartrace.complementrun", 19 | "onView:vartrace.sidebar" 20 | ], 21 | "main": "./dist/index.js", 22 | "contributes": { 23 | "commands": [ 24 | { 25 | "command": "extension.vartrace.complementrun", 26 | "title": "vartrace - analyze this file as a script", 27 | "category": "debug" 28 | }, 29 | { 30 | "command": "extension.vartrace.setVarNameFromCursor", 31 | "title": "vartrace - trace this variable", 32 | "category": "debug" 33 | }, 34 | { 35 | "command": "extension.vartrace.run", 36 | "title": "vartrace - run analysis by current config", 37 | "category": "debug" 38 | } 39 | ], 40 | "viewsContainers": { 41 | "activitybar": [ 42 | { 43 | "id": "vartrace-sidebar", 44 | "title": "Vartrace", 45 | "icon": "resources/vt.png" 46 | } 47 | ] 48 | }, 49 | "views": { 50 | "vartrace-sidebar": [ 51 | { 52 | "type": "webview", 53 | "id": "vartrace.sidebar", 54 | "name": "VarTrace" 55 | } 56 | ] 57 | }, 58 | "menus": { 59 | "editor/context": [ 60 | { 61 | "when": "editorFocus", 62 | "command": "extension.vartrace.complementrun", 63 | "group": "VarTrace@1" 64 | }, 65 | { 66 | "when": "editorFocus", 67 | "command": "extension.vartrace.run", 68 | "group": "VarTrace@2" 69 | }, 70 | { 71 | "when": "varTrace.logLoaded", 72 | "command": "extension.vartrace.setVarNameFromCursor", 73 | "group": "VarTrace@3" 74 | } 75 | ] 76 | } 77 | }, 78 | "scripts": { 79 | "vscode:prepublish": "npm run build", 80 | "build": "npm-run-all build:*", 81 | "watch": "npm-run-all --parallel watch:*", 82 | "build:webpack": "webpack", 83 | "build:dumper": "cpx \"src/dumper/*.*\" dist/dumper", 84 | "watch:webpack": "webpack -w --mode development", 85 | "watch:dumper": "cpx -w \"src/dumper/{*/*,*}\" dist/dumper", 86 | "test:prepare": "npm run back-rebuild", 87 | "test:core": "node ./node_modules/jest/bin/jest.js test/core.test.ts", 88 | "test:dump": "node ./node_modules/jest/bin/jest.js test/dump.test.ts", 89 | "package:windows": "vsce package --target win32-x64", 90 | "package:linux": "vsce package --target linux-x64", 91 | "back-rebuild": "npm uninstall better-sqlite3 && npm install better-sqlite3", 92 | "rebuild": "electron-rebuild -v 17.4.0 -f -w better-sqlite3" 93 | }, 94 | "devDependencies": { 95 | "@types/better-sqlite3": "^7.5.0", 96 | "@types/glob": "^7.1.4", 97 | "@types/jest": "^27.0.2", 98 | "@types/node": "^17.0.32", 99 | "@types/react": "^17.0.26", 100 | "@types/react-dom": "^17.0.9", 101 | "@types/react-redux": "^7.1.17", 102 | "@types/sqlite3": "^3.1.7", 103 | "@types/vscode": "^1.60.0", 104 | "@typescript-eslint/eslint-plugin": "^4.32.0", 105 | "@typescript-eslint/parser": "^4.32.0", 106 | "@types/babel__core": "^7.1.16", 107 | "cpx": "^1.5.0", 108 | "cross-env": "^7.0.3", 109 | "css-loader": "^5.2.7", 110 | "electron": "^13.5.1", 111 | "electron-rebuild": "^3.2.7", 112 | "eslint": "^7.32.0", 113 | "glob": "^7.2.0", 114 | "jest": "^27.5.0", 115 | "license-checker": "^25.0.1", 116 | "npm-run-all": "^4.1.5", 117 | "style-loader": "^2.0.0", 118 | "ts-essentials": "^9.0.0", 119 | "ts-jest": "^27.1.3", 120 | "ts-loader": "^9.2.6", 121 | "typescript": "^4.4.3", 122 | "vscode-test": "^1.6.1", 123 | "webpack": "^5.56.0", 124 | "webpack-cli": "^4.8.0" 125 | }, 126 | "dependencies": { 127 | "@babel/core": "^7.15.8", 128 | "@babel/preset-env": "^7.15.8", 129 | "@babel/preset-typescript": "^7.15.0", 130 | "@emotion/react": "^11.6.0", 131 | "@emotion/styled": "^11.6.0", 132 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 133 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 134 | "@fortawesome/react-fontawesome": "^0.1.16", 135 | "@material-ui/core": "^4.12.4", 136 | "@mui/icons-material": "^5.2.0", 137 | "@mui/lab": "^5.0.0-alpha.56", 138 | "@mui/material": "^5.2.0", 139 | "@reduxjs/toolkit": "^1.6.1", 140 | "babel-jest": "^27.3.1", 141 | "better-sqlite3": "^7.5.1", 142 | "file-uri-to-path": "^2.0.0", 143 | "kysely": "^0.16.9", 144 | "material-ui-popup-state": "^1.9.3", 145 | "react": "^17.0.2", 146 | "react-dom": "^17.0.2", 147 | "react-id-generator": "^3.0.2", 148 | "react-redux": "^7.2.5", 149 | "redux": "^4.1.0", 150 | "sqlite": "^4.0.23", 151 | "string-argv": "^0.3.1" 152 | }, 153 | "jest": { 154 | "extensionsToTreatAsEsm": [ 155 | ".ts" 156 | ], 157 | "testRegex": "/*/.*\\.test\\.ts$", 158 | "moduleFileExtensions": [ 159 | "ts", 160 | "tsx", 161 | "js", 162 | "jsx", 163 | "json", 164 | "node" 165 | ] 166 | } 167 | } -------------------------------------------------------------------------------- /resources/howtorun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zat-dev/VarTrace/c3c4b8c5b021b68bca8b77340686f89bbe430963/resources/howtorun.png -------------------------------------------------------------------------------- /resources/printf_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zat-dev/VarTrace/c3c4b8c5b021b68bca8b77340686f89bbe430963/resources/printf_demo.gif -------------------------------------------------------------------------------- /resources/step_back_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zat-dev/VarTrace/c3c4b8c5b021b68bca8b77340686f89bbe430963/resources/step_back_demo.gif -------------------------------------------------------------------------------- /resources/vt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zat-dev/VarTrace/c3c4b8c5b021b68bca8b77340686f89bbe430963/resources/vt.png -------------------------------------------------------------------------------- /src/codeLang/index.ts: -------------------------------------------------------------------------------- 1 | import { python } from "./python" 2 | 3 | 4 | export const getCodeLang = (lang: string) => { 5 | switch (lang) { 6 | case "python": return python 7 | default: 8 | throw new Error("unsupported language") 9 | } 10 | } -------------------------------------------------------------------------------- /src/codeLang/python/deserialize.ts: -------------------------------------------------------------------------------- 1 | import { Value } from '../../core'; 2 | import { makeValueText } from '../../core/entity/value'; 3 | import { isContainer, isIgnored, isPrimitive, Val } from './variableValue'; 4 | type Syntax = { 5 | prefix: string, 6 | suffix: string, 7 | sep: string, 8 | keyValueSep?: string 9 | } 10 | 11 | const getSyntax = (typeName: string): Syntax => { 12 | switch (typeName) { 13 | case "list": return { prefix: "[", suffix: "]", sep: ", " } 14 | case "tuple": return { prefix: "(", suffix: ")", sep: ", " } 15 | case "set": return { prefix: "{", suffix: "}", sep: ", " } 16 | case "dict": return { prefix: "{", suffix: "}", sep: ", ", keyValueSep: ": " } 17 | } 18 | return { prefix: `${typeName}(`, suffix: ")", sep: ", " } 19 | } 20 | 21 | const concatChildren = (typeName: string, children: string[]) => { 22 | const { prefix, suffix, sep } = getSyntax(typeName) 23 | const content = children.join(sep) 24 | return `${prefix}${content}${suffix}` 25 | } 26 | 27 | 28 | export const stringifyVal = (val: Val): string => { 29 | if (val === null) { 30 | return "None" 31 | } 32 | if (isPrimitive(val)) { 33 | return JSON.stringify(val) 34 | } 35 | switch (val.dataType) { 36 | case "list": 37 | case "set": { 38 | const children = val.data.map((v, _i) => stringifyVal(v)) 39 | const typeName = val.typeName 40 | return concatChildren(typeName, children) 41 | } 42 | case "dict": { 43 | const typeName = val.typeName 44 | const children = 45 | Object.entries(val.data) 46 | .map(([key, v]) => `${key}: ${stringifyVal(v)}`) 47 | return concatChildren(typeName, children) 48 | } 49 | case "reference": { 50 | return `ignored(circular ref)` 51 | } 52 | case "escaped": { 53 | return val.data 54 | } 55 | case "ignored": 56 | return `ignored(${val.reason})` 57 | } 58 | } 59 | 60 | 61 | 62 | const convertLogValAux = (val: Val): Value => { 63 | if (!isContainer(val)) { 64 | const expression = [makeValueText(stringifyVal(val))] 65 | const ignoredBy = isIgnored(val) ? val.reason : undefined 66 | let typeName = 67 | val === null ? 68 | "None" 69 | : isPrimitive(val) ? 70 | typeof val 71 | : val.dataType 72 | 73 | return { 74 | children: {}, 75 | expression, 76 | type: typeName, 77 | hasHit: false, 78 | ignoredBy 79 | } 80 | } 81 | // construct from children results 82 | const syntax = getSyntax(val.typeName) 83 | let expression = [makeValueText(syntax.prefix)] 84 | let children: Value["children"] = {} 85 | 86 | for (const [k, v] of Object.entries(val.data)) { 87 | const childValue = convertLogValAux(v) 88 | const keyExpression = [makeValueText(k)] 89 | children[k] = { 90 | keyExpression, 91 | value: childValue 92 | } 93 | if (syntax.keyValueSep) { 94 | expression.push(...keyExpression) 95 | expression.push(makeValueText(syntax.keyValueSep)) 96 | } 97 | expression.push(...childValue.expression) 98 | expression.push(makeValueText(syntax.sep)) 99 | } 100 | const suffix = makeValueText(syntax.suffix) 101 | if (expression.length > 1) { 102 | // replace last syntax.sep (of for loop) to suffix 103 | expression[expression.length - 1] = suffix 104 | } else { 105 | // empty children 106 | expression.push(suffix) 107 | } 108 | return { 109 | children, 110 | expression, 111 | type: val.typeName, 112 | hasHit: false 113 | } 114 | } 115 | 116 | export const deserialize = (val: string): Value => { 117 | 118 | return convertLogValAux(JSON.parse(val)) 119 | 120 | } -------------------------------------------------------------------------------- /src/codeLang/python/index.ts: -------------------------------------------------------------------------------- 1 | import { CodeLang } from "../../core/entity/codeLang"; 2 | import { deserialize } from "./deserialize"; 3 | 4 | 5 | export const python: CodeLang = { 6 | deserialize 7 | } -------------------------------------------------------------------------------- /src/codeLang/python/variableValue.ts: -------------------------------------------------------------------------------- 1 | export type Annotation = "new" | "changed" 2 | 3 | 4 | export type Container = { 5 | dataType: T, 6 | id: number 7 | typeName: string 8 | data: D, 9 | annotation: Annotation[] // used by analysis before/after dump 10 | } 11 | 12 | export type ListContainer = Container<"list", Val[]> 13 | export type SetContainer = Container<"set", Val[]> 14 | export type DictContainer = Container<"dict", { [key: string]: Val }> 15 | 16 | export type ContainerVal = 17 | | ListContainer 18 | | SetContainer 19 | | DictContainer 20 | 21 | export type Primitive = null | number | string | boolean 22 | 23 | export type Val = 24 | | Primitive 25 | | ContainerVal 26 | | Reference 27 | | Ignored 28 | | Escaped 29 | 30 | export type Ignored = { 31 | dataType: "ignored" 32 | typeName: string, 33 | reason: string, 34 | annotation: Annotation[] // used by analysis before/after dump 35 | } 36 | export type Reference = { 37 | dataType: "reference" 38 | id: number, 39 | annotation: Annotation[] // used by analysis before/after dump 40 | } 41 | 42 | export type EscapedData = "inf" | "-inf" | "nan" | "undef" | string 43 | 44 | export type Escaped = { 45 | dataType: "escaped" 46 | typeName: string 47 | data: EscapedData, 48 | annotation: Annotation[] // used by analysis before/after dump 49 | } 50 | 51 | 52 | 53 | export const isPrimitive = (val: Val): val is Primitive => { 54 | if (val === null) { 55 | return true 56 | } 57 | return ["number", "boolean", "string"].includes(typeof val) 58 | } 59 | 60 | export const isContainer = (val: Val): val is ContainerVal => { 61 | if (isPrimitive(val)) { 62 | return false 63 | } 64 | return ["list", "dict", "set"].includes(val.dataType) 65 | } 66 | export const isIgnored = (val: Val): val is Ignored => { 67 | if (isPrimitive(val)) { 68 | return false 69 | } 70 | if (isContainer(val)) { 71 | return false 72 | } 73 | return val.dataType === "ignored" 74 | } -------------------------------------------------------------------------------- /src/core/entity/codeLang.ts: -------------------------------------------------------------------------------- 1 | import { Value } from "./value"; 2 | 3 | export interface CodeLang { 4 | deserialize: (raw: string) => Value 5 | } -------------------------------------------------------------------------------- /src/core/entity/logLoader.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ValueText, Value } from "./value" 3 | 4 | // valid range is from 0 to getMaxStep result 5 | export type Step = number 6 | // line No of code(1 origin) 7 | export type CodeLine = number 8 | 9 | export type ScopeKind = Scope 10 | 11 | export type StepVariables = { 12 | // key is any identifier string of variable 13 | // id number string or `${scopeName}-${varName}`...etc. 14 | // upstream code can be depend on only uniqueness about key 15 | [key: string]: { 16 | val: Value 17 | varName: ValueText[], 18 | scopeKind: ScopeKind, 19 | scopeName: string 20 | } 21 | } 22 | 23 | export type StepInfo = { 24 | step: Step, 25 | stepKind: string, 26 | fileAbsPath: string, 27 | line: number, 28 | functionName: string, 29 | returnVal: Value | undefined 30 | } 31 | 32 | export type LogLoadStatus = "running" | "completed" 33 | 34 | export type Metadata = { 35 | language: string, 36 | maxStep: number, 37 | version: string, 38 | format: string, 39 | status: LogLoadStatus, 40 | basePath: string 41 | } 42 | 43 | export type VarChangeLog = { 44 | step: number, 45 | val: Value, 46 | varId: number, 47 | varName: ValueText[], 48 | scopeName: string, 49 | scopeKind: ScopeKind, 50 | prevVal: Value | undefined 51 | }[] 52 | 53 | export type ScopeLog = StepInfo[] 54 | 55 | export type SrcFile = { 56 | absPath: string, 57 | modTimestamp: Date // unix time 58 | } 59 | 60 | // hide raw log structure 61 | export interface LogLoader { 62 | validate(rawLog: unknown): boolean 63 | getStepInfo(step: Step): Promise 64 | getMetadata(): Promise 65 | getFiles(): Promise 66 | getLineSteps(fileAbsPath: string, line: number): Promise 67 | getStepVariables(step: Step): Promise 68 | getVarChangeLog(): Promise 69 | close(): Promise 70 | } 71 | -------------------------------------------------------------------------------- /src/core/entity/scope.ts: -------------------------------------------------------------------------------- 1 | 2 | type Scope = "local" | "global" 3 | -------------------------------------------------------------------------------- /src/core/entity/value.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export type ValueText = { 5 | isNew?: boolean, // undefined means false 6 | isChanged?: boolean, // undefined means false 7 | // half-open intervals: end is exclusive 8 | hit: { start: number, end: number }[], 9 | text: string, 10 | } 11 | 12 | export const isPrimitive = (value: Value) => { 13 | for (let _ in value.children) { return false } 14 | return true 15 | } 16 | 17 | export const isEqual = (value1: Value, value2: Value) => { 18 | if (value1.expression.length !== value2.expression.length) { 19 | return false 20 | } 21 | return value1.expression.every((v, i) => value2.expression[i]?.text == v.text) 22 | } 23 | 24 | export const makeValueText = (text: string): ValueText => ({ 25 | hit: [], 26 | text, 27 | }) 28 | 29 | // because of performance reason, 30 | // parent and children value should be the same instance 31 | // in order to parent hit annotation sync to children 32 | export type Value = { 33 | expression: ValueText[], 34 | children: { 35 | [key: string]: { 36 | keyExpression: ValueText[], 37 | value: Value 38 | } 39 | }, 40 | type: string, 41 | isSet?: boolean, // undefined means false 42 | hasHit?: boolean, // undefined means false 43 | ignoredBy?: string // reason for ignored value 44 | } 45 | 46 | export const stringify = (value: Value) => { 47 | return value.expression.map(({ text }) => text).join("") 48 | } 49 | -------------------------------------------------------------------------------- /src/core/feature/lineSteps.ts: -------------------------------------------------------------------------------- 1 | import { ValueText, Value, makeValueText } from "../entity/value" 2 | import { LogLoader } from "../entity/logLoader" 3 | import { decorateDiff } from "../lib/decorateDiff" 4 | import { addHitToTexts, addHitToValue } from "../lib/search" 5 | 6 | 7 | 8 | export type Query = { 9 | fileAbsPath: string, 10 | line: number, 11 | } 12 | 13 | export type Result = number[] 14 | 15 | export const getLineSteps = async (logLoader: LogLoader, query: Query): Promise => { 16 | return await logLoader.getLineSteps(query.fileAbsPath, query.line) 17 | } -------------------------------------------------------------------------------- /src/core/feature/logMetadata.ts: -------------------------------------------------------------------------------- 1 | import { LogLoader, Metadata } from "../entity/logLoader" 2 | 3 | export type Result = Metadata | undefined 4 | 5 | export const getMetadata = async (logLoader: LogLoader): Promise => { 6 | return await logLoader.getMetadata() 7 | } -------------------------------------------------------------------------------- /src/core/feature/srcFiles.ts: -------------------------------------------------------------------------------- 1 | import { LogLoader } from "../entity/logLoader" 2 | 3 | export const getFiles = async (logLoader: LogLoader) => { 4 | return await logLoader.getFiles() 5 | } 6 | 7 | // windows is case insensitive but linux is sensitive. 8 | // currently fallback case insensitive if no case sensitive match 9 | // TODO: save platform info into dump log and desice by it. 10 | export const normalizePathByLog = async (targetAbsPath: string, logLoader: LogLoader): Promise => { 11 | const files = await (await logLoader.getFiles()).map(x => x.absPath) 12 | if (files.includes(targetAbsPath)) { 13 | return targetAbsPath 14 | } 15 | const lowerTarget = targetAbsPath.toLowerCase() 16 | for (const file of files) { 17 | if (file.toLowerCase() === lowerTarget) { 18 | return file 19 | } 20 | } 21 | return undefined 22 | } -------------------------------------------------------------------------------- /src/core/feature/stepInfo.ts: -------------------------------------------------------------------------------- 1 | import { Value } from "../entity/value" 2 | import { LogLoader } from "../entity/logLoader" 3 | import { decorateDiff } from "../lib/decorateDiff" 4 | 5 | export type Result = { 6 | returnVal: Value | undefined; 7 | step: number; 8 | stepKind: string; 9 | fileAbsPath: string; 10 | line: number; 11 | functionName: string; 12 | } 13 | 14 | export const getStepInfo = async (logLoader: LogLoader, step: number): Promise => { 15 | const stepInfo = await logLoader.getStepInfo(step) 16 | let returnVal = stepInfo.returnVal 17 | if (returnVal !== undefined) { 18 | decorateDiff(returnVal, undefined) 19 | } 20 | return { 21 | ...stepInfo, 22 | returnVal 23 | } 24 | } -------------------------------------------------------------------------------- /src/core/feature/stepVars.ts: -------------------------------------------------------------------------------- 1 | import { Value, ValueText } from "../entity/value" 2 | import { LogLoader } from "../entity/logLoader" 3 | import { decorateDiff } from "../lib/decorateDiff" 4 | import { addHitToTexts } from "../lib/search" 5 | 6 | export type Query = { 7 | step: number, 8 | varNameFilter: string, 9 | showIgnored?: boolean, 10 | showFilterNotMatch?: boolean, 11 | } 12 | 13 | export type Result = { 14 | [scope in Scope]: { 15 | varName: ValueText[], 16 | before: Value | undefined, 17 | after: Value | undefined 18 | }[] 19 | } 20 | 21 | export const getStepVars = async (logLoader: LogLoader, query: Query) => { 22 | const { step, varNameFilter, showIgnored, showFilterNotMatch } = query 23 | const currentVariables = await logLoader.getStepVariables(step) 24 | const nextVariables = await logLoader.getStepVariables(step + 1) 25 | const keys = new Set([...Object.keys(currentVariables), ...Object.keys(nextVariables)]) 26 | let result: Result = { 27 | "local": [], 28 | "global": [] 29 | } 30 | for (const key of keys) { 31 | const current = currentVariables[key] 32 | const next = nextVariables[key] 33 | const varName = current?.varName ?? next?.varName! 34 | const scopeKind = current?.scopeKind ?? next?.scopeKind! 35 | const before = current?.val 36 | const after = next?.val 37 | if (!showIgnored) { 38 | const isBeforeIgnoredOrUndef = 39 | before === undefined || before.ignoredBy !== undefined 40 | const isAfterIgnoredOrUndef = 41 | after === undefined || after.ignoredBy !== undefined 42 | if (isBeforeIgnoredOrUndef && isAfterIgnoredOrUndef) { 43 | continue 44 | } 45 | } 46 | if (before) { 47 | decorateDiff(before, after) 48 | } 49 | if (after) { 50 | decorateDiff(after, before) 51 | } 52 | let scope = scopeKind 53 | 54 | if (varNameFilter) { 55 | const hasHit = addHitToTexts(varName, varNameFilter) 56 | if (!showFilterNotMatch && !hasHit) { 57 | continue 58 | } 59 | } 60 | result[scope].push({ varName, before, after }) 61 | } 62 | return result 63 | } 64 | -------------------------------------------------------------------------------- /src/core/feature/varChangeLog.ts: -------------------------------------------------------------------------------- 1 | import { decorateDiff } from "../lib/decorateDiff" 2 | import { ValueText, Value } from "../entity/value" 3 | import { paging, Paging } from "../lib/paging" 4 | import { addHitToTexts, addHitToValue, clearHitFromText, clearHitFromValue } from "../lib/search" 5 | import { LogLoader, LogLoadStatus, VarChangeLog } from "../entity/logLoader" 6 | import { abbrivateValue } from "../lib/abbrivate" 7 | import { Cache } from "../lib/cache" 8 | export type Query = { 9 | varNameFilter?: string, 10 | valueFilter?: string, 11 | showIgnored?: boolean, 12 | showFilterNotMatch?: boolean, 13 | page: number, 14 | pageSize: number 15 | } 16 | 17 | type Entry = { 18 | step: number, 19 | scopeName: string, 20 | scopeKind: Scope, 21 | varId: number, 22 | varName: ValueText[], 23 | val: Value 24 | } 25 | 26 | export type Result = Paging 27 | 28 | const isStopped = async (logLoader: LogLoader) => { 29 | const metadatat = await logLoader.getMetadata() 30 | const stoppedStatus: (LogLoadStatus | undefined)[] = ["completed"] 31 | return stoppedStatus.includes(metadatat?.status) 32 | } 33 | 34 | const getResultFromLog = async (logLoader: LogLoader) => { 35 | let result = await logLoader.getVarChangeLog() 36 | for (const entry of result) { 37 | decorateDiff(entry.val, entry.prevVal) 38 | } 39 | return result 40 | } 41 | 42 | const clearHitFromResult = (result: VarChangeLog) => { 43 | for (const entry of result) { 44 | entry.varName.forEach(x => clearHitFromText(x)) 45 | clearHitFromValue(entry.val) 46 | if (entry.prevVal) { 47 | clearHitFromValue(entry.prevVal) 48 | } 49 | } 50 | } 51 | 52 | export const getvarChangeLog = async (logLoader: LogLoader, cache: Cache, query: Query): Promise => { 53 | const cacheKey = "varChangeLog" 54 | let result: VarChangeLog = cache.get(cacheKey) ?? await getResultFromLog(logLoader) 55 | if (!cache.has(cacheKey) && await isStopped(logLoader)) { 56 | cache.add(cacheKey, result) 57 | } 58 | // performance reason, var change log is cached and mutable change might remain 59 | clearHitFromResult(result) 60 | if (!query.showIgnored) { 61 | result = result.filter(x => x.val.type !== "ignored" || query.showIgnored) 62 | } 63 | 64 | if (query.varNameFilter) { 65 | const varNameFilter = query.varNameFilter 66 | result = result.filter(entry => 67 | addHitToTexts(entry.varName, varNameFilter) 68 | || query.showFilterNotMatch) 69 | } 70 | 71 | if (query.valueFilter) { 72 | const valueFilter = query.valueFilter 73 | result = result.filter(entry => 74 | addHitToValue(entry.val, valueFilter) 75 | || query.showFilterNotMatch) 76 | } 77 | 78 | const pagingResult = paging(result, query.page, query.pageSize) 79 | const abbrivatedContents = 80 | pagingResult.contents.map(x => ({ 81 | ...x, 82 | val: abbrivateValue(x.val), 83 | prevVal: x.prevVal ? abbrivateValue(x.prevVal) : undefined 84 | })) 85 | return { 86 | maxPage: pagingResult.maxPage, 87 | page: pagingResult.page, 88 | contents: abbrivatedContents 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import { LogLoader } from "./entity/logLoader" 2 | import * as logMetadata from "./feature/logMetadata" 3 | import { getFiles, normalizePathByLog } from "./feature/srcFiles" 4 | import * as varChangeLog from "./feature/varChangeLog" 5 | import * as lineVars from "./feature/lineSteps" 6 | import * as stepVars from "./feature/stepVars" 7 | import * as stepInfo from "./feature/stepInfo" 8 | 9 | export { ValueText, Value } from "./entity/value" 10 | export type VarChangeLog = varChangeLog.Result 11 | export type LineVars = lineVars.Result 12 | export type StepVars = stepVars.Result 13 | export type StepInfo = stepInfo.Result 14 | export type Metadata = logMetadata.Result 15 | import { Cache } from "./lib/cache" 16 | export class VarTrace { 17 | cache: Cache = new Cache() 18 | 19 | constructor(private logLoader: LogLoader) { 20 | 21 | } 22 | normalizePathByLog = async (fileAbsPath: string) => { 23 | return await normalizePathByLog(fileAbsPath, this.logLoader) 24 | } 25 | getFiles = async () => { 26 | return await getFiles(this.logLoader) 27 | } 28 | getMetadata = async () => { 29 | return await logMetadata.getMetadata(this.logLoader) 30 | } 31 | getVarChangeLog = async (query: varChangeLog.Query) => { 32 | return varChangeLog.getvarChangeLog(this.logLoader, this.cache, query) 33 | } 34 | getLineSteps = async (query: lineVars.Query) => { 35 | return lineVars.getLineSteps(this.logLoader, query) 36 | } 37 | getStepVars = async (query: stepVars.Query) => { 38 | return stepVars.getStepVars(this.logLoader, query) 39 | } 40 | getStepInfo = async (step: number) => { 41 | return stepInfo.getStepInfo(this.logLoader, step) 42 | } 43 | 44 | close = async () => { 45 | await this.logLoader.close() 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/core/lib/abbrivate.ts: -------------------------------------------------------------------------------- 1 | import { Value, isPrimitive, makeValueText } from "../entity/value"; 2 | 3 | const threshold = 30 4 | 5 | export const abbrivateValue = (target: Value): Value => { 6 | if (isPrimitive(target)) { 7 | return target 8 | } 9 | let children: typeof target.children = {} 10 | for (const [key, child] of Object.entries(target.children)) { 11 | children[key] = { 12 | keyExpression: child.keyExpression, 13 | value: abbrivateValue(child.value) 14 | } 15 | } 16 | const valueLen = target.expression.length 17 | if (valueLen < threshold) { 18 | return { 19 | ...target, 20 | children 21 | } 22 | } 23 | const abbrivatedExpression = [ 24 | target.expression[0]!, 25 | makeValueText("..."), 26 | target.expression[valueLen - 1]! 27 | ] 28 | return { 29 | ...target, 30 | children, 31 | expression: abbrivatedExpression 32 | } 33 | } -------------------------------------------------------------------------------- /src/core/lib/cache.ts: -------------------------------------------------------------------------------- 1 | export class Cache { 2 | cache: { [key: string]: any } = {} 3 | 4 | add = (key: string, data: any) => { 5 | this.cache[key] = data 6 | } 7 | get = (key: string) => { 8 | return this.cache[key] 9 | } 10 | has = (key: string) => { 11 | return key in this.cache 12 | } 13 | } -------------------------------------------------------------------------------- /src/core/lib/decorateDiff.ts: -------------------------------------------------------------------------------- 1 | import { Value, isEqual, isPrimitive } from "../entity/value" 2 | 3 | 4 | const setNew = (target: Value) => { 5 | for (const txt of target.expression) { 6 | txt.isNew = true 7 | } 8 | } 9 | 10 | const setChanged = (target: Value) => { 11 | for (const txt of target.expression) { 12 | txt.isChanged = true 13 | } 14 | } 15 | 16 | export const decorateDiff = (target: Value, compare: Value | undefined) => { 17 | if (compare === undefined) { 18 | setNew(target) 19 | return 20 | } 21 | if (target.type !== compare.type) { 22 | setChanged(target) 23 | return 24 | } 25 | if (isPrimitive(target)) { 26 | if (!isEqual(target, compare)) { 27 | setChanged(target) 28 | } 29 | return 30 | } 31 | 32 | if (target.isSet) { 33 | // (hash)set elem can have different key 34 | for (const child of Object.values(target.children)) { 35 | if (Object.values(compare.children).some(x => isEqual(x.value, child.value))) { 36 | continue 37 | } 38 | setNew(child.value) 39 | } 40 | return 41 | } 42 | for (const [i, child] of Object.entries(target.children)) { 43 | decorateDiff(child.value, compare.children[i]?.value) 44 | } 45 | } -------------------------------------------------------------------------------- /src/core/lib/paging.ts: -------------------------------------------------------------------------------- 1 | export type Paging = { 2 | page: number, 3 | maxPage: number, 4 | contents: T[] 5 | } 6 | 7 | export const paging = (array: T[], page: number, pageSize: number): Paging => { 8 | const maxLen = array.length 9 | const maxPage = Math.ceil(maxLen / pageSize) 10 | return { 11 | maxPage, 12 | page, 13 | contents: array.slice(pageSize * (page - 1), pageSize * page) 14 | } 15 | } -------------------------------------------------------------------------------- /src/core/lib/search.ts: -------------------------------------------------------------------------------- 1 | import { ValueText, Value, isPrimitive } from "../entity/value"; 2 | 3 | export const clearHitFromText = (decoTxt: ValueText) => { 4 | decoTxt.hit = [] 5 | } 6 | 7 | export const clearHitFromValue = (value: Value) => { 8 | value.hasHit = false 9 | value.expression.forEach(clearHitFromText) 10 | } 11 | 12 | 13 | const addHitToDecoTxt = (decoTxt: ValueText, searchString: string) => { 14 | const text = decoTxt.text 15 | // clear previous result 16 | decoTxt.hit = [] 17 | const regexp = new RegExp(searchString, 'g') 18 | const matches = text.matchAll(regexp) 19 | 20 | for (const match of matches) { 21 | // note: although type errors, index, match[0] never null 22 | // https://github.com/microsoft/TypeScript/issues/36788 23 | decoTxt.hit.push({ start: match.index!, end: match.index! + match[0]!.length }) 24 | } 25 | return decoTxt.hit.length > 0 26 | } 27 | 28 | export const addHitToTexts = (texts: ValueText[], searchString: string) => { 29 | let hasHit = false 30 | for (let text of texts) { 31 | hasHit ||= addHitToDecoTxt(text, searchString) 32 | } 33 | return hasHit 34 | } 35 | 36 | export const addHitToValue = (value: Value, searchString: string) => { 37 | let hasHit = false 38 | 39 | hasHit ||= addHitToTexts(value.expression, searchString) 40 | 41 | 42 | for (let child of Object.values(value.children)) { 43 | hasHit ||= addHitToTexts(child.keyExpression, searchString) 44 | hasHit ||= addHitToValue(child.value, searchString) 45 | } 46 | value.hasHit = hasHit 47 | return hasHit 48 | } 49 | -------------------------------------------------------------------------------- /src/dumper/create_tracelog_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE metadata ( 2 | version TEXT, 3 | language TEXT, 4 | status TEXT, 5 | max_step INTEGER, 6 | base_path TEXT 7 | ); 8 | 9 | CREATE TABLE files ( 10 | file_id INTEGER, 11 | file_abs_path TEXT NOT NULL UNIQUE, 12 | mod_timestamp INTEGER NOT NULL, /* unix time */ 13 | PRIMARY KEY(file_id) 14 | ); 15 | 16 | CREATE TABLE functions ( 17 | function_id INTEGER, 18 | function_name TEXT UNIQUE, 19 | PRIMARY KEY(function_id) 20 | ); 21 | 22 | CREATE TABLE steps ( 23 | step INTEGER, 24 | step_kind TEXT, /* ex: line(statement), exception, return, call */ 25 | file_id INTEGER, 26 | line INTEGER NOT NULL, 27 | function_id INTEGER, 28 | return_snap JSON, /* retrun value of function or throwed exception value */ 29 | local_scope_id INTEGER, 30 | global_scope_id INTEGER, 31 | PRIMARY KEY(step), 32 | FOREIGN KEY(file_id) REFERENCES files(file_id), 33 | FOREIGN KEY(function_id) REFERENCES functions(function_id), 34 | FOREIGN KEY(local_scope_id) REFERENCES scopes(scope_id), 35 | FOREIGN KEY(global_scope_id) REFERENCES scopes(scope_id) 36 | ); 37 | 38 | CREATE TABLE scopes ( 39 | scope_id INTEGER, 40 | scope_name TEXT NOT NULL, 41 | scope_kind TEXT, 42 | start INTEGER NOT NULL, 43 | end INTEGER, 44 | PRIMARY KEY(scope_id), 45 | UNIQUE(scope_name, scope_kind, start) 46 | ); 47 | CREATE TABLE variables ( 48 | var_id INTEGER, 49 | var_name TEXT NOT NULL, 50 | defined_step INTEGER NOT NULL, 51 | scope_id INTEGER, 52 | PRIMARY KEY(var_id), 53 | FOREIGN KEY(scope_id) REFERENCES scopes(scope_id) 54 | ); 55 | CREATE TABLE variable_values ( 56 | step INTEGER, 57 | var_id INTEGER, 58 | value JSON, 59 | prevValue JSON, 60 | PRIMARY KEY(step, var_id), 61 | FOREIGN KEY(var_id) REFERENCES variables(var_id) 62 | ); 63 | 64 | PRAGMA journal_mode = wal; 65 | PRAGMA synchronous = off; 66 | PRAGMA temp_store = memory; 67 | PRAGMA mmap_size = 30000000000; /* about 30GB */ 68 | PRAGMA foreign_keys = true; -------------------------------------------------------------------------------- /src/dumper/lib.ts: -------------------------------------------------------------------------------- 1 | 2 | // helper functions to run dumper from nodejs(vscode) 3 | import * as fs from "fs" 4 | import * as path from "path" 5 | import stringArgv from 'string-argv' 6 | 7 | type DumpOption = { 8 | optionName: "targetDir" | "targetModule" | "stdin", 9 | value: string 10 | } 11 | 12 | export type DumpConfig = { 13 | language: "python", 14 | execCommand: string, 15 | options: DumpOption[] 16 | } 17 | 18 | export const getDefaultDumpConfig = (lang: SupportLang) => { 19 | const defaults: { [key in SupportLang]: DumpConfig } = { 20 | "python": { 21 | language: "python", 22 | execCommand: "", 23 | options: [{ 24 | optionName: "targetModule", 25 | value: "__main__" 26 | }] 27 | } 28 | } 29 | return defaults[lang] 30 | } 31 | 32 | type SupportLang = "python" 33 | 34 | const convertOptions = (options: DumpOption[]) => { 35 | let optionArgs = "" 36 | let stdin = "" 37 | for (const { optionName, value } of options) { 38 | switch (optionName) { 39 | case "stdin": 40 | stdin += value 41 | stdin += "\n" 42 | break 43 | case "targetDir": 44 | optionArgs += ` -d ${value}` 45 | break 46 | case "targetModule": 47 | optionArgs += ` -M ${value}` 48 | break 49 | default: 50 | throw new NeverCaseError(optionName) 51 | } 52 | } 53 | return { stdin, optionArgs } 54 | } 55 | export const prepareLogPath = (outFile: string) => { 56 | const parentDir = path.dirname(outFile) 57 | if (!fs.existsSync(parentDir)) { 58 | fs.mkdirSync(parentDir, { recursive: true }) 59 | } 60 | 61 | if (fs.existsSync(outFile)) { 62 | fs.unlinkSync(outFile) 63 | } 64 | } 65 | 66 | const pythonExePattern = "(python|py)([.0-9]*)?(exe)?" 67 | 68 | export const complementExecCommand = (conf: DumpConfig, file: string) => { 69 | const execArgs = stringArgv(conf.execCommand) 70 | switch (conf.language) { 71 | case "python": 72 | const pythonIndex = execArgs.findIndex(arg => (new RegExp(`^${pythonExePattern}$`)).test(arg)) 73 | if (pythonIndex === -1) { 74 | return undefined 75 | } 76 | return `${execArgs[pythonIndex]} ${file}` 77 | default: 78 | throw new NeverCaseError(conf.language) 79 | } 80 | } 81 | 82 | 83 | const makeDumpCommand = (dumperRootAbsPath: string, conf: DumpConfig, outPath: string) => { 84 | const execArgs = stringArgv(conf.execCommand) 85 | switch (conf.language) { 86 | case "python": 87 | // detect key positions 88 | const pythonIndex = execArgs.findIndex(arg => (new RegExp(`^${pythonExePattern}$`)).test(arg)) 89 | const targetIndex = execArgs.findIndex((arg, i) => i > pythonIndex && !arg.startsWith("-")) 90 | const isModuleRun = execArgs[targetIndex - 1] === "-m" 91 | const insertDumperIndex = isModuleRun ? targetIndex - 1 : targetIndex 92 | 93 | const { optionArgs, stdin } = convertOptions(conf.options) 94 | const dumperPath = path.join(dumperRootAbsPath, 'python/main.py') 95 | const dumperOptions = [ 96 | dumperPath, 97 | ` -o ${outPath} `, 98 | optionArgs 99 | ].join(" ") 100 | // construct modified command by inserting args 101 | execArgs.splice(insertDumperIndex, 0, dumperOptions) 102 | let command = execArgs.join(" ") 103 | 104 | if (stdin) { 105 | command += "\n" 106 | command += stdin 107 | } 108 | return { 109 | command, 110 | prepare: (logPath: string) => { 111 | prepareLogPath(logPath) 112 | // pycache sometimes conflicts when multiple python versions are installed 113 | // to avoid this, remove cache of vartrace 114 | // __pycache__ path might be changed. but not support now. 115 | const cachePath = path.join(dumperRootAbsPath, "python/__pycache__") 116 | if (fs.existsSync(cachePath)) { 117 | fs.rmSync(cachePath, { recursive: true }) 118 | } 119 | 120 | } 121 | } 122 | default: 123 | throw new NeverCaseError(conf.language) 124 | } 125 | } 126 | 127 | 128 | 129 | export const runDump = ( 130 | dumperRootAbsPath: string, 131 | runner: (command: string) => void, 132 | conf: DumpConfig, 133 | logPath: string 134 | ) => { 135 | 136 | const { command, prepare } = makeDumpCommand(dumperRootAbsPath, conf, logPath) 137 | prepare(logPath) 138 | // to input stdin to user script, dumper runs on terminal(VSCode) 139 | runner(command) 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/dumper/python/config.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import os 3 | import argparse 4 | 5 | 6 | @dataclasses.dataclass 7 | class Config: 8 | out_file: str 9 | step_limit: int 10 | size_limit: int 11 | target_modules: list 12 | dirs: list 13 | module_mode: bool 14 | main: str 15 | args: list 16 | 17 | def __init__(self): 18 | default_output = 'vtlog.db' 19 | 20 | parser = argparse.ArgumentParser( 21 | description='trace variables in execution') 22 | parser.add_argument("-o", "--output", default=default_output, 23 | help="trace result output file") 24 | parser.add_argument("-s", "--step_limit", default=30000, 25 | help="maximum steps to abort") 26 | parser.add_argument("-S", "--size_limit", default=500, 27 | help="maximum size to abort") 28 | parser.add_argument('-M', '--target-module', action='append', 29 | default=[], 30 | help='additonal modules whose class instances will be traced') 31 | parser.add_argument('-d', '--dir', action='append', 32 | default=[], 33 | type=os.path.abspath, 34 | help='directory which contains target files') 35 | parser.add_argument('-m', '--module-mode', action='store_true', 36 | help='run as module mode') 37 | parser.add_argument("main", help='trace target entry file or module') 38 | parser.add_argument("main_args", nargs=argparse.REMAINDER, 39 | help='trace target command arguments') 40 | args = parser.parse_args() 41 | self.out_file = args.output 42 | self.step_limit = args.step_limit 43 | self.size_limit = args.size_limit 44 | self.target_modules = args.target_module 45 | self.module_mode = args.module_mode 46 | self.dirs = args.dir 47 | self.main = args.main 48 | self.args = args.main_args 49 | -------------------------------------------------------------------------------- /src/dumper/python/dump_handler.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import copy 3 | from msilib.schema import Error 4 | import os 5 | import sys 6 | import json 7 | import ast 8 | from serializer import serialize 9 | from sqlite_saver import SqliteSaver 10 | 11 | version = "0.0.2" 12 | 13 | 14 | class DumpHandler(): 15 | __module__ = "VarTrace" 16 | 17 | def __init__(self, config): 18 | self.config = config 19 | self.target_variables_dic = {} 20 | self.file_ids = {} 21 | self.saver = SqliteSaver(config.out_file) 22 | self.step = 0 23 | self.var_id = 0 24 | self.last_scope_id = 0 25 | self.local_scope_id_stack = [] 26 | self.global_scope_ids = {} 27 | self.function_ids = {} 28 | self.this_script_dir = os.path.dirname(__file__) 29 | # var_id_dic[scope_id][var_name] := var_id 30 | self.var_id_dic = defaultdict(dict) 31 | # var_id -> val 32 | self.prev_serialized_variables = defaultdict(lambda: None) 33 | self.__init_save_data() 34 | 35 | def __proc_file(self, file_path): 36 | if file_path in self.file_ids: 37 | return 38 | file_id = len(self.file_ids) 39 | self.file_ids[file_path] = file_id 40 | self.target_variables_dic[file_path] = self.__extract_var_names( 41 | file_path) 42 | mod_timestamp = os.path.getmtime(file_path) 43 | self.saver.save_file( 44 | file_id, os.path.abspath(file_path), int(mod_timestamp)) 45 | 46 | def __is_target_file(self, file_path): 47 | for dir_path in self.config.dirs: 48 | if dir_path == os.path.commonpath(dir_path, file_path): 49 | return True 50 | return False 51 | 52 | def __is_target_module(self, module_name: str): 53 | if module_name == None: 54 | return False 55 | for target in self.config.target_modules: 56 | if module_name.startswith(target): 57 | return True 58 | return False 59 | 60 | def __is_target(self, file_path, module_name): 61 | if file_path.startswith(self.this_script_dir): 62 | return False 63 | if file_path == "": 64 | return False 65 | return self.__is_target_module(module_name) or self.__is_target_file(file_path) 66 | 67 | def __init_save_data(self): 68 | self.saver.init_metadata(version) 69 | 70 | def __extract_var_names(self, file_name): 71 | with open(file_name, encoding="utf-8") as file: 72 | source = file.read() 73 | var_names = set() 74 | source_ast = ast.parse(source, type_comments=True) 75 | for node in ast.walk(source_ast): 76 | if not isinstance(node, ast.Name): 77 | continue 78 | var_names.add(node.id) 79 | return var_names 80 | 81 | def __proc_func(self, function_name): 82 | if function_name in self.function_ids: 83 | return 84 | function_id = len(self.function_ids) 85 | self.function_ids[function_name] = function_id 86 | self.saver.save_function(function_id, function_name) 87 | 88 | def __proc_var_id(self, scope_id, var_name): 89 | if var_name in self.var_id_dic[scope_id]: 90 | return 91 | self.saver.save_variable( 92 | self.var_id, var_name, self.step, scope_id) 93 | self.var_id_dic[scope_id][var_name] = self.var_id 94 | self.var_id += 1 95 | 96 | def __get_var_id(self, scope_id, var_name): 97 | self.__proc_var_id(scope_id, var_name) 98 | return self.var_id_dic[scope_id][var_name] 99 | 100 | def __proc_step_info(self, frame, step_kind, return_val): 101 | file_path = frame.f_code.co_filename 102 | function_name = frame.f_code.co_name 103 | module_name = frame.f_globals["__name__"] 104 | local_scope_id = self.local_scope_id_stack[-1] 105 | 106 | global_scope_id = self.global_scope_ids[module_name] 107 | 108 | self.saver.update_max_step(self.step) 109 | 110 | file_id = self.file_ids[file_path] 111 | function_id = self.function_ids[function_name] 112 | serialized_return = None 113 | if step_kind == "exception" or step_kind == "return": 114 | serialized_return = serialize(return_val, self.config.size_limit) 115 | 116 | self.saver.save_step( 117 | self.step, 118 | step_kind, 119 | file_id, 120 | frame.f_lineno, 121 | function_id, 122 | serialized_return, 123 | local_scope_id, 124 | global_scope_id) 125 | 126 | def __new_local(self, scope_name): 127 | self.last_scope_id += 1 128 | self.local_scope_id_stack.append(self.last_scope_id) 129 | self.saver.save_scope( 130 | self.last_scope_id, scope_name, "local", self.step, None) 131 | 132 | def __end_local(self): 133 | scope_id = self.local_scope_id_stack.pop() 134 | self.saver.update_scope(scope_id, self.step-1) 135 | 136 | def __new_global(self, module_name): 137 | if not module_name in self.global_scope_ids: 138 | self.last_scope_id += 1 139 | self.global_scope_ids[module_name] = self.last_scope_id 140 | self.saver.save_scope( 141 | self.last_scope_id, module_name, "global", self.step, None) 142 | 143 | def __is_local_not_global(self, function_name): 144 | return function_name != "" 145 | 146 | def __proc_new_scope(self, step_kind, function_name, module_name): 147 | self.__new_global(module_name) 148 | if step_kind != "call": 149 | return 150 | 151 | if self.__is_local_not_global(function_name): 152 | self.__new_local(function_name) 153 | else: 154 | global_id = self.global_scope_ids[module_name] 155 | self.local_scope_id_stack.append(global_id) 156 | 157 | def __dump_scope_vars(self, scope_id, variables, target_var_names): 158 | serialized_variables = [] 159 | for var_name, raw_val in variables: 160 | if var_name not in target_var_names: 161 | continue 162 | var_id = self.__get_var_id(scope_id, var_name) 163 | serialized = serialize(raw_val, self.config.size_limit) 164 | prev_serialized = self.prev_serialized_variables[var_id] 165 | serialized_variables.append( 166 | (self.step, var_id, serialized, prev_serialized)) 167 | self.prev_serialized_variables[var_id] = serialized 168 | self.saver.save_variable_values(serialized_variables) 169 | 170 | def __proc_vars(self, frame, file_name, module_name): 171 | local_scope_id = self.local_scope_id_stack[-1] 172 | global_scope_id = self.global_scope_ids[module_name] 173 | 174 | target_var_names = self.target_variables_dic[file_name] 175 | 176 | self.__dump_scope_vars( 177 | local_scope_id, frame.f_locals.items(), target_var_names) 178 | 179 | if local_scope_id == global_scope_id: 180 | return 181 | self.__dump_scope_vars( 182 | global_scope_id, frame.f_globals.items(), target_var_names) 183 | 184 | def __proc_end_scope(self, step_kind, function_name): 185 | if step_kind == "return" and self.__is_local_not_global(function_name): 186 | self.__end_local() 187 | 188 | def dump(self, step_kind, frame, return_val=None): 189 | file_path = frame.f_code.co_filename 190 | function_name = frame.f_code.co_name 191 | module_name = frame.f_globals["__name__"] 192 | 193 | if not self.__is_target(file_path, module_name): 194 | return 195 | 196 | if self.step > self.config.step_limit: 197 | print("stop by vartrace: step_limit exceeded", file=sys.stderr) 198 | exit(1) 199 | 200 | self.__proc_new_scope(step_kind, function_name, module_name) 201 | self.__proc_file(file_path) 202 | self.__proc_func(function_name) 203 | self.__proc_step_info(frame, step_kind, return_val) 204 | self.__proc_vars(frame, file_path, module_name) 205 | self.saver.flush() 206 | self.__proc_end_scope(step_kind, function_name) 207 | 208 | self.step += 1 209 | 210 | def finish(self, reason): 211 | while self.local_scope_id_stack: 212 | scope_id = self.local_scope_id_stack.pop() 213 | self.saver.update_scope(scope_id, self.step) 214 | for _, scope_id in self.global_scope_ids.items(): 215 | self.saver.update_scope(scope_id, self.step) 216 | self.saver.update_scope(0, self.step) 217 | 218 | self.saver.finish() 219 | -------------------------------------------------------------------------------- /src/dumper/python/main.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import atexit 3 | import os 4 | import sys 5 | import signal 6 | from dump_handler import DumpHandler 7 | from config import Config 8 | 9 | version = "0.0.2" 10 | 11 | 12 | def imitate_sys(config: Config): 13 | # imitate direct execution main_file 14 | sys.path[0] = os.path.dirname(config.main) 15 | sys.argv[:] = [config.main] + config.args # args passed to main_file 16 | 17 | 18 | def main(): 19 | config = Config() 20 | 21 | imitate_sys(config) 22 | 23 | if config.module_mode: 24 | import runpy 25 | mod_name, mod_spec, code = runpy._get_module_details(config.main) 26 | cmd_globals = { 27 | "__name__": "__main__", 28 | "__file__": config.main, 29 | "__package__": mod_name, 30 | "__loader__": mod_spec.loader, 31 | "__spec__": mod_spec, 32 | "__builtins__": __builtins__, 33 | } 34 | else: 35 | with open(config.main, encoding='utf-8') as f: 36 | src = f.read() 37 | cmd_globals = { 38 | '__name__': '__main__', 39 | "__file__": config.main, 40 | "__builtins__": __builtins__, 41 | } 42 | code = compile(src, config.main, "exec") 43 | 44 | dump_handler = DumpHandler(config) 45 | 46 | def exit_dump(): 47 | dump_handler.finish("exit") 48 | atexit.register(exit_dump) 49 | 50 | def trace_dispatch(frame, event, arg): 51 | try: 52 | if event == 'line': 53 | dump_handler.dump("line", frame) 54 | elif event == 'call': 55 | dump_handler.dump("call", frame) 56 | elif event == 'return': 57 | dump_handler.dump("return", frame, arg) 58 | elif event == 'exception': 59 | dump_handler.dump("exception", frame, repr(arg[1])) 60 | # do nothing on c_call, c_exception, c_return 61 | except Exception as e: 62 | msg_format = "faital error in vartrace at file: {} line: {}" 63 | msg = msg_format.format( 64 | frame.f_code.co_filename, frame.f_lineno, frame.f_lineno) 65 | print(msg, file=sys.stderr) 66 | print(traceback.format_exc(), file=sys.stderr) 67 | exit(1) 68 | return trace_dispatch 69 | 70 | def sig_handler(signum, frame): 71 | dump_handler.finish("signal") 72 | atexit.unregister(exit_dump) 73 | exit(1) 74 | 75 | signal.signal(signal.SIGTERM, sig_handler) 76 | signal.signal(signal.SIGINT, sig_handler) 77 | 78 | sys.settrace(trace_dispatch) 79 | exec(code, cmd_globals) 80 | 81 | 82 | if __name__ == '__main__': 83 | main() 84 | -------------------------------------------------------------------------------- /src/dumper/python/serializer.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import json 3 | 4 | bigint = 1 << 53 # 53 is Javascript Max Int size 5 | escape_float = [float("INF"), -float("INF"), float("NaN")] 6 | list_container_types = [list, tuple, range, deque] 7 | ignore_types = ["module", "function", "type"] 8 | 9 | ignore_result = '{{"dataType":"ignored","typeName":"{}","reason":"{}"}}' 10 | bigint_result = '{{"typeName":"bigint","dataType":"escaped","data":{}}}' 11 | float_escape_result = '{{"typeName":"float","dataType":"escaped","data":"{}"}}' 12 | primitive_result = '{{"dataType":"primitive","typeName":"{}","data":{}}}' 13 | ref_result = '{{"dataType":"reference","data":{}}}' 14 | list_header = '{{"dataType":"list","typeName":"{}","data":[' 15 | dict_header = '{{"dataType":"dict","typeName":"{}","data":{{' 16 | 17 | 18 | class Comma(): 19 | pass 20 | 21 | 22 | class Coron(): 23 | pass 24 | 25 | 26 | class ListFooter(): 27 | pass 28 | 29 | 30 | class DictFooter(): 31 | pass 32 | 33 | 34 | class DictKey(): 35 | pass 36 | 37 | 38 | commna = Comma() 39 | coron = Coron() 40 | list_footer = ListFooter() 41 | dict_footer = DictFooter() 42 | dict_key = DictKey() 43 | 44 | 45 | def serialize(val, size_limit): 46 | ids = set() 47 | result = [] 48 | stack = [val] 49 | while stack: 50 | target = stack.pop() 51 | target_type = type(target) 52 | type_name = target_type.__name__ 53 | 54 | if target_type == Comma: 55 | result.append(",") 56 | elif target_type == Coron: 57 | result.append(":") 58 | elif target_type == DictKey: 59 | key = stack.pop() 60 | if type(key) == str: 61 | key = json.dumps(json.dumps(key)) 62 | result.append(key) 63 | else: 64 | result.append('"{}"'.format(key)) 65 | elif target_type == int: 66 | if target >= bigint: 67 | result.append(bigint_result.format(str(target))) 68 | else: 69 | result.append(str(target)) 70 | elif target_type == str: 71 | if len(target) > size_limit: 72 | result.append(ignore_result.format(type_name, "huge")) 73 | continue 74 | result.append(json.dumps(target)) 75 | elif target_type == bool: 76 | data = "true" if target else "false" 77 | result.append(data) 78 | elif type_name in ignore_types: 79 | result.append(ignore_result.format(type_name, "not target")) 80 | elif target is None: 81 | result.append("null") 82 | elif target_type == ListFooter: 83 | result.append("]}") 84 | elif target_type == DictFooter: 85 | result.append("}}") 86 | 87 | elif target_type == float: 88 | if target in escape_float: 89 | result.append(float_escape_result.format(str(target))) 90 | else: 91 | result.append(str(target)) 92 | else: 93 | target_id = id(target) 94 | if target_id in ids: 95 | result.append(ref_result.format(target_id)) 96 | continue 97 | ids.add(target_id) 98 | if target_type in list_container_types \ 99 | or target_type.__base__ in list_container_types: 100 | if len(target) > size_limit: 101 | result.append(ignore_result.format(type_name, "huge")) 102 | continue 103 | result.append(list_header.format(type_name)) 104 | stack.append(list_footer) 105 | for child in reversed(target): 106 | stack.extend(( 107 | child, 108 | commna 109 | )) 110 | if stack[-1] == commna: 111 | stack.pop() 112 | elif target_type == set or target_type.__base__ == set: 113 | if len(target) > size_limit: 114 | result.append(ignore_result.format(type_name, "huge")) 115 | continue 116 | result.append(list_header.format(type_name)) 117 | stack.append(list_footer) 118 | for child in target: 119 | stack.extend(( 120 | child, 121 | commna 122 | )) 123 | if stack[-1] == commna: 124 | stack.pop() 125 | elif target_type == dict or target_type.__base__ == dict: 126 | if len(target) > size_limit: 127 | result.append(ignore_result.format(type_name, "huge")) 128 | continue 129 | result.append(dict_header.format(type_name)) 130 | stack.append(dict_footer) 131 | for k, v in target.items(): 132 | stack.extend(( 133 | v, 134 | coron, 135 | k, 136 | dict_key, 137 | commna 138 | )) 139 | if stack[-1] == commna: 140 | stack.pop() 141 | elif hasattr(target, '__dict__'): 142 | result.append(dict_header.format(type_name)) 143 | stack.append(dict_footer) 144 | for k, v in vars(target).items(): 145 | if callable(v): 146 | continue 147 | stack.extend(( 148 | v, 149 | coron, 150 | k, 151 | dict_key, 152 | commna 153 | )) 154 | if stack[-1] == commna: 155 | stack.pop() 156 | else: 157 | result.append(ignore_result.format(type_name, "unsupported")) 158 | 159 | return "".join(result) 160 | -------------------------------------------------------------------------------- /src/dumper/python/sqlite_saver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | 4 | 5 | class SqliteSaver(): 6 | this_script_dir = os.path.dirname(__file__) 7 | init_sql_file = os.path.join( 8 | this_script_dir, "../create_tracelog_table.sql") 9 | base_path = os.getcwd() 10 | 11 | def __init__(self, target_file): 12 | self.con = sqlite3.connect(target_file) 13 | self.cursor = self.con.cursor() 14 | with open(self.init_sql_file) as sql_file: 15 | init_sql = sql_file.read() 16 | self.cursor.executescript(init_sql) 17 | 18 | def init_metadata(self, version): 19 | self.cursor.execute( 20 | 'INSERT INTO metadata VALUES (?, ?, ?, 0, ?)', (version, "python", "running", self.base_path)) 21 | self.flush() 22 | 23 | def update_max_step(self, step): 24 | self.cursor.execute( 25 | 'UPDATE metadata SET max_step = ? WHERE language = "python"', (step, )) 26 | 27 | def save_file(self, file_id, file_abs_path, mod_timestamp): 28 | self.cursor.execute( 29 | 'INSERT INTO files VALUES (?, ?, ?)', (file_id, file_abs_path, mod_timestamp)) 30 | 31 | def save_function(self, function_id, function_name): 32 | self.cursor.execute( 33 | 'INSERT INTO functions VALUES (?, ?)', (function_id, function_name)) 34 | 35 | def save_step(self, step, step_kind, file_id, line, function_id, return_snap, local_scope_id, global_scope_id): 36 | self.cursor.execute( 37 | 'INSERT INTO steps VALUES (?, ?, ?, ?, ?, ?, ?, ?)', 38 | (step, step_kind, file_id, line, function_id, 39 | return_snap, local_scope_id, global_scope_id) 40 | ) 41 | 42 | def save_variable(self, var_id, var_name, defined_step, scope_id): 43 | self.cursor.execute( 44 | 'INSERT INTO variables VALUES (?, ?, ?, ?)', (var_id, var_name, defined_step, scope_id)) 45 | 46 | def save_scope(self, scope_id, scope_name, scope_kind, start, end): 47 | self.cursor.execute( 48 | 'INSERT INTO scopes VALUES (?, ?, ?, ?, ?)', (scope_id, scope_name, scope_kind, start, end)) 49 | 50 | def update_scope(self, scope_id, end): 51 | self.cursor.execute( 52 | 'UPDATE scopes SET end = ? WHERE scope_id = ?', (end, scope_id)) 53 | 54 | def save_variable_values(self, variable_values): 55 | self.cursor.executemany( 56 | 'INSERT INTO variable_values VALUES (?, ?, ?, ?)', variable_values) 57 | 58 | def flush(self): 59 | self.con.commit() 60 | 61 | def finish(self): 62 | self.cursor.execute( 63 | 'UPDATE metadata SET status = ? WHERE language = "python"', ("completed", )) 64 | self.con.commit() 65 | self.con.close() 66 | -------------------------------------------------------------------------------- /src/logLoader/sqliteLogLoader.ts: -------------------------------------------------------------------------------- 1 | import { Metadata, StepInfo, StepVariables, LogLoader, VarChangeLog, ScopeKind, SrcFile } from "../core/entity/logLoader" 2 | 3 | import { 4 | Kysely, 5 | SqliteDialect, 6 | } from 'kysely' 7 | import { CodeLang } from "../core/entity/codeLang" 8 | import { getCodeLang } from "../codeLang" 9 | import { makeValueText } from "../core/entity/value" 10 | 11 | /** 12 | * ref. DB schema: dumper/create_tracelog_table.sql 13 | */ 14 | 15 | interface Database { 16 | metadata: { 17 | version: string, 18 | language: string, 19 | status: "running" | "completed", 20 | max_step: number, 21 | base_path: string 22 | } 23 | files: { 24 | file_id: number, 25 | mod_timestamp: Date, 26 | file_abs_path: string 27 | } 28 | functions: { 29 | function_id: number, 30 | function_name: string 31 | } 32 | steps: { 33 | step: number, 34 | step_kind: string, /* ex: line(statement), exception, return, call */ 35 | file_id: number, 36 | line: number, 37 | function_id: number, 38 | local_scope_id: number, 39 | global_scope_id: number, 40 | return_snap: string | null, /* retrun value of function or throwed exception value */ 41 | } 42 | scopes: { 43 | scope_id: number, 44 | scope_name: string, 45 | scope_kind: ScopeKind, 46 | start: number, 47 | end: number 48 | } 49 | variables: { 50 | var_id: number, 51 | var_name: string, 52 | defined_step: number, 53 | scope_id: number, 54 | } 55 | variable_values: { 56 | step: number, 57 | var_id: number, 58 | value: string, 59 | prevValue: string | null, 60 | } 61 | } 62 | 63 | export class SqliteLogLoarder implements LogLoader { 64 | db 65 | constructor(databasePath: string) { 66 | this.db = new Kysely({ 67 | dialect: new SqliteDialect({ 68 | databasePath 69 | }) 70 | }) 71 | } 72 | getCodeLang = async (): Promise => { 73 | const metadata = await this.getMetadata() 74 | if (metadata === undefined) { 75 | return 76 | } 77 | return getCodeLang(metadata.language) 78 | } 79 | validate(rawLog: unknown): boolean { 80 | return true 81 | } 82 | getDecodeLogVal = async () => { 83 | const codeLang = await this.getCodeLang() 84 | if (!codeLang) { 85 | return undefined 86 | } 87 | return (target: string | null) => { 88 | if (target === null) return undefined 89 | return codeLang.deserialize(target) 90 | } 91 | } 92 | getStepInfo = async (step: number): Promise => { 93 | const result = await this.db.selectFrom('steps') 94 | .innerJoin('files', 'files.file_id', 'steps.file_id') 95 | .innerJoin('functions', 'functions.function_id', 'steps.function_id') 96 | .select([ 97 | 'step', 98 | 'step_kind as stepKind', 99 | 'file_abs_path as fileAbsPath', 100 | 'line', 101 | 'function_name as functionName', 102 | 'return_snap as returnSnap' 103 | ]) 104 | .where('step', '=', step) 105 | .executeTakeFirstOrThrow() 106 | const decode = await this.getDecodeLogVal() 107 | const returnVal = decode ? decode(result.returnSnap) : undefined 108 | return (({ returnSnap, ...rest }) => ({ ...rest, returnVal }))(result) 109 | } 110 | getMetadata = async (): Promise => { 111 | const result = await this.db.selectFrom('metadata') 112 | .select([ 113 | 'language', 114 | 'version', 115 | 'max_step as maxStep', 116 | 'status', 117 | 'base_path as basePath' 118 | ]) 119 | .executeTakeFirst() 120 | if (!result) return undefined 121 | return { ...result, format: "sqlite" } 122 | } 123 | getFiles = async (): Promise => { 124 | const result = await this.db.selectFrom('files') 125 | .select([ 126 | 'file_abs_path as absPath', 127 | 'mod_timestamp as modTimestamp' 128 | ]) 129 | .execute() 130 | if (!result) return [] 131 | return result 132 | } 133 | getLineSteps = async (fileAbsPath: string, line: number): Promise => { 134 | const result = await this.db 135 | .with('file_ids', db => db.selectFrom('files') 136 | .select('file_id').where('file_abs_path', '=', fileAbsPath)) 137 | .selectFrom('file_ids') 138 | .innerJoin('steps', 'file_ids.file_id', 'steps.file_id') 139 | .select('step') 140 | .where('line', '=', line) 141 | .execute() 142 | if (!result) return [] 143 | return result.map(({ step }) => step) 144 | } 145 | getStepVariables = async (step: number): Promise => { 146 | const queryResults = await this.db 147 | .selectFrom('variable_values') 148 | .innerJoin('variables', 'variable_values.var_id', 'variables.var_id') 149 | .innerJoin('scopes', 'scopes.scope_id', 'variables.scope_id') 150 | .select([ 151 | "scope_name as scopeName", 152 | "scope_kind as scopeKind", 153 | "var_name as varName", 154 | "variable_values.var_id as varId", 155 | "value" 156 | ]) 157 | .where("step", "=", step) 158 | .execute() 159 | let result: StepVariables = {} 160 | const decode = await this.getDecodeLogVal() 161 | if (!decode) { 162 | return result 163 | } 164 | for (const { value, varId, varName, ...rest } of queryResults) { 165 | result[varId.toString()] = { 166 | ...rest, 167 | varName: [makeValueText(varName)], 168 | val: decode(value)! 169 | } 170 | } 171 | 172 | return result 173 | } 174 | getVarChangeLog = async (): Promise => { 175 | 176 | const queryResults = await this.db 177 | .selectFrom('variable_values') 178 | .innerJoin('variables', 'variable_values.var_id', 'variables.var_id') 179 | .innerJoin('scopes', 'variables.scope_id', 'scopes.scope_id') 180 | .select([ 181 | 'variable_values.step', 182 | 'scope_name as scopeName', 183 | 'scope_kind as scopeKind', 184 | 'var_name as varName', 185 | 'variable_values.var_id as varId', 186 | 'value', 187 | 'prevValue']) 188 | .whereRef('value', '!=', 'prevValue') 189 | .orWhere('prevValue', 'is', null) 190 | .execute() 191 | 192 | const decode = await this.getDecodeLogVal() 193 | if (!decode) { 194 | return [] 195 | } 196 | const results = [] 197 | for (const result of queryResults) { 198 | const val = decode(result.value) 199 | if (val === undefined) { 200 | throw Error("unexpected undefined val") 201 | } 202 | // recoeded step is before line execution. 203 | // so step when prev != current, it means the change made before step 204 | const changedStep = Math.max(result.step - 1, 0) 205 | results.push({ 206 | step: changedStep, 207 | val: val, 208 | prevVal: decode(result.prevValue), 209 | scopeKind: result.scopeKind, 210 | scopeName: result.scopeName, 211 | varName: [makeValueText(result.varName)], 212 | varId: result.varId 213 | }) 214 | } 215 | 216 | return results 217 | 218 | } 219 | close = async (): Promise => { 220 | await this.db.destroy() 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /src/util/utilityTypes.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/type-challenges/type-challenges 2 | type LookUp = U extends K ? U : never 3 | 4 | type DeepReadonly = { 5 | readonly [P in keyof T]: (keyof T[P]) extends never ? T[P] : DeepReadonly 6 | } 7 | 8 | type Entries = K extends keyof T ? [K, Required[K]] : never 9 | 10 | type TupleToUnion = T[number] 11 | type RequiredByKeys = Omit> & Omit, never> 12 | 13 | type Exact = T extends U ? U extends T ? T : never : never 14 | 15 | // https://stackoverflow.com/questions/41253310/typescript-retrieve-element-type-information-from-array-type 16 | type ElemOf = 17 | ArrayType extends readonly (infer Elem)[] ? Elem : never; 18 | 19 | /* 20 | https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript/52913382 21 | 22 | exhaustive check by type. add the below code at the end of switch statement 23 | default: 24 | throw new UnreachableCaseError(message) 25 | */ 26 | class NeverCaseError extends Error { 27 | constructor(val: never) { 28 | super(`never case: ${JSON.stringify(val)}`); 29 | } 30 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | 4 | import * as vscode from "vscode"; 5 | import { init } from "./init"; 6 | 7 | 8 | 9 | export function activate(context: vscode.ExtensionContext) { 10 | init(context) 11 | } 12 | 13 | // this method is called when your extension is deactivated 14 | export function deactivate() { } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/init.ts: -------------------------------------------------------------------------------- 1 | import * as dumpConf from "./processors/dumpConf" 2 | import * as breakPoint from "./processors/breakPoint" 3 | import * as logFile from "./processors/logFile" 4 | import * as varChangeLog from "./processors/varChangeLog" 5 | import * as stepDetail from "./processors/step" 6 | import * as valueShowOption from "./processors/valueShowOption" 7 | import * as panelHandle from "./processors/panelHandle" 8 | import * as vscode from "vscode"; 9 | import * as store from "./store/store" 10 | import { StateGetter } from "./store/state" 11 | import * as EditorPanel from "./uiWrapper/editorPanel" 12 | import { WebviewViewProvider } from "./uiWrapper/webviewView" 13 | import { WebviewPanel } from "./uiWrapper/webviewPanel" 14 | import { inform } from "./uiWrapper/notification" 15 | import { CancelByFailure, CancelByUser } from "./processors/common" 16 | 17 | type Panels = Parameters[0] 18 | 19 | const initProcs = (context: vscode.ExtensionContext, panels: Panels) => { 20 | logFile.initialize(context.extensionPath) 21 | stepDetail.initialize() 22 | panelHandle.initialize(panels) 23 | breakPoint.initialize() 24 | varChangeLog.initialize() 25 | dumpConf.initialize() 26 | valueShowOption.initialize() 27 | } 28 | 29 | const createUiWrappers = (context: vscode.ExtensionContext) => { 30 | return { 31 | "sidebar": new WebviewViewProvider("sidebar", context), 32 | panels: { 33 | "step detail": new WebviewPanel("step detail", context), 34 | "variable change log": new WebviewPanel("variable change log", context), 35 | } 36 | } 37 | } 38 | 39 | const registerCommand = (context: vscode.ExtensionContext, command: string, action: () => Promise) => { 40 | let disposable = vscode.commands.registerCommand( 41 | command, 42 | () => { 43 | try { 44 | action() 45 | } 46 | catch (e) { 47 | if (e instanceof CancelByFailure) { 48 | inform(`${e}`) 49 | } 50 | else if (e instanceof CancelByUser) { 51 | // user cancel shows nothing 52 | } 53 | else { 54 | inform(`unhandled exception occured: ${e}`) 55 | } 56 | } 57 | } 58 | ); 59 | context.subscriptions.push(disposable); 60 | } 61 | 62 | const registerVscodeCall = (context: vscode.ExtensionContext) => { 63 | registerCommand(context, "extension.vartrace.complementrun", async () => { 64 | await store.callProc("dumpConf/userInput", "complementExecCommand") 65 | await store.callProc("logFile/varTrace", "dump") 66 | await store.callProc("panel/open", "openStepDetail") 67 | await store.callProc("panel/open", "openVarChangeLog") 68 | }) 69 | registerCommand(context, "extension.vartrace.setVarNameFromCursor", async () => { 70 | await store.callProc("panel/open", "openVarChangeLog") 71 | await store.callProc("varChangeLog/userInput", "setVarNameFromCursor") 72 | await store.callProc("varChangeLog/result", "load") 73 | }) 74 | registerCommand(context, "extension.vartrace.run", async () => { 75 | await store.callProc("logFile/varTrace", "dump") 76 | await store.callProc("panel/open", "openStepDetail") 77 | await store.callProc("panel/open", "openVarChangeLog") 78 | }) 79 | vscode.debug.onDidChangeBreakpoints( 80 | () => store.callProc("breakPoints/breakPoints", "readFromVscode") 81 | ) 82 | } 83 | 84 | export const init = (context: vscode.ExtensionContext) => { 85 | registerVscodeCall(context) 86 | const uiWrappers = createUiWrappers(context) 87 | initProcs(context, uiWrappers.panels) 88 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/messageHandler.ts: -------------------------------------------------------------------------------- 1 | import { encodeDisplayMessage, isCallMessage, isSubscribeMessage, isUpdateMessage } from "../messaging" 2 | import { CancelByFailure, CancelByUser } from "./processors/common" 3 | import { addSubscriber, callProc, updateState } from "./store/store" 4 | import { inform } from "./uiWrapper/notification" 5 | 6 | // message sender should have post method to receive reply 7 | type MessageSender = { 8 | key: string, 9 | post: (message: any) => void, 10 | } 11 | 12 | export const handleMessage = (message: any, sender: MessageSender) => { 13 | try { 14 | if (isSubscribeMessage(message)) { 15 | const stateId = message.stateId 16 | const subscriber = (data: any) => { 17 | const message = encodeDisplayMessage(stateId, data) 18 | sender.post(message) 19 | } 20 | addSubscriber(stateId, sender.key, subscriber) 21 | } 22 | else if (isCallMessage(message)) { 23 | const { stateId, procName } = message 24 | callProc(stateId, procName) 25 | } 26 | else if (isUpdateMessage(message)) { 27 | const { stateId, data } = message 28 | updateState(stateId, data) 29 | } 30 | } catch (e) { 31 | if (e instanceof CancelByFailure) { 32 | inform(`${e}`) 33 | } 34 | else if (e instanceof CancelByUser) { 35 | // user cancel shows nothing 36 | } 37 | else { 38 | inform(`unhandled exception occured: ${e}`) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/breakPoint.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StateGetter, StateGetterOf } from "../store/state" 3 | import { addProc, addState, ExposeToWebview } from "../store/store" 4 | import * as editorPanel from "../uiWrapper/editorPanel" 5 | import { varTraceState, metadataState } from "./logFile" 6 | import * as core from "../../../core" 7 | import { reloadShowOptionsState } from "./valueShowOption" 8 | import { CancelByFailure } from "./common" 9 | 10 | const domain = "breakPoints" 11 | 12 | type UserInput = { [fileAbsPath: string]: number[] } 13 | 14 | const breakPointsState = addState(domain, "breakPoints", { 15 | userEditable: true as const, 16 | init: { breakPoints: {} as UserInput }, 17 | depends: [metadataState, varTraceState] 18 | }) 19 | 20 | const breakPointsProcs = addProc(breakPointsState, { 21 | "readFromVscode": { 22 | triggers: [varTraceState], 23 | proc: async (get, set) => { 24 | const breakPoints = editorPanel.getBreakPoints() 25 | set({ breakPoints }) 26 | } 27 | } 28 | }) 29 | 30 | type Result = { 31 | [fileAbsPath: string]: { 32 | [line: number]: number[] 33 | } 34 | } 35 | 36 | const breakPointStepsState = addState(domain, "breakPointSteps", { 37 | userEditable: false as const, 38 | init: { 39 | breakPointSteps: {} as Result 40 | }, 41 | depends: [varTraceState, breakPointsState, metadataState] 42 | }) 43 | 44 | 45 | const breakPointStepsProcs = addProc(breakPointStepsState, { 46 | "load": { 47 | triggers: [metadataState, breakPointsState, varTraceState], 48 | proc: async (get, set) => { 49 | const { varTrace } = get(varTraceState) 50 | if (!varTrace) { 51 | throw new CancelByFailure("can not access analysis result") 52 | } 53 | const { breakPoints } = get(breakPointsState) 54 | let breakPointSteps: Result = {} 55 | for (const [rawFileAbsPath, lines] of Object.entries(breakPoints)) { 56 | const fileAbsPath = await varTrace.normalizePathByLog(rawFileAbsPath) 57 | if (fileAbsPath === undefined) { 58 | continue 59 | } 60 | let fileResult: { [line: number]: number[] } = {} 61 | for (const line of lines) { 62 | fileResult[line] = await varTrace.getLineSteps({ 63 | fileAbsPath, 64 | line 65 | }) 66 | } 67 | breakPointSteps[fileAbsPath] = fileResult 68 | } 69 | 70 | 71 | set({ breakPointSteps }) 72 | } 73 | } 74 | }) 75 | 76 | export const initialize = () => { 77 | // dummy for webpack not to remove this module 78 | return 79 | } 80 | 81 | export type BreakPoints = 82 | ExposeToWebview 83 | 84 | export type BreakPointSteps = 85 | ExposeToWebview 86 | -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/common.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class CancelByUser extends Error { 4 | 5 | } 6 | 7 | export class CancelByFailure extends Error { 8 | 9 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/dumpConf.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import { FileSystemPicker } from "../uiWrapper/fileSystemPicker"; 3 | import { complementExecCommand, getDefaultDumpConfig } from "../../../dumper/lib"; 4 | import { addState, addProc, ExposeToWebview } from "../store/store"; 5 | import * as vscode from "vscode"; 6 | import { inform } from "../uiWrapper/notification"; 7 | import { CancelByFailure, CancelByUser } from "./common"; 8 | const domain = "dumpConf" 9 | 10 | 11 | export const dumpConfState = addState(domain, "userInput", { 12 | userEditable: true as const, 13 | init: getDefaultDumpConfig("python"), 14 | depends: [] 15 | }) 16 | 17 | const procs = addProc(dumpConfState, { 18 | "load": { 19 | proc: async (_get, set) => { 20 | const fileSystemPath = await FileSystemPicker.selectFile() 21 | const content = fs.readFileSync(fileSystemPath, 'utf8'); 22 | const userInput = JSON.parse(content) 23 | set(userInput) 24 | } 25 | }, 26 | "complementExecCommand": { 27 | proc: async (get, set) => { 28 | const userInput = get(dumpConfState) 29 | const activeFile = vscode.window.activeTextEditor?.document.fileName ?? "" 30 | if (!activeFile) { 31 | throw new CancelByFailure("tried complementing execution command. but opened active file not found") 32 | } 33 | 34 | let execCommand = complementExecCommand(userInput, activeFile) 35 | if (execCommand === undefined) { 36 | const execInput = await 37 | vscode.window.showInputBox({ title: "input your executable(ex: python, python3)" }) 38 | if (execInput === undefined) { 39 | throw new CancelByUser() 40 | } 41 | execCommand = `${execInput} ${activeFile}` 42 | } 43 | 44 | set({ ...userInput, execCommand }) 45 | inform(`execution command is complemented by ${activeFile}`) 46 | } 47 | }, 48 | "save": { 49 | proc: async (get) => { 50 | const userInput = get(dumpConfState) 51 | const fileSystemPath = await FileSystemPicker.selectSavePath() 52 | const content = JSON.stringify(userInput) 53 | fs.writeFileSync(fileSystemPath, content, 'utf8'); 54 | } 55 | } 56 | }) 57 | 58 | 59 | export const initialize = () => { 60 | // dummy for webpack not to remove this module 61 | } 62 | 63 | export type DumpConf = ExposeToWebview -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/logFile.ts: -------------------------------------------------------------------------------- 1 | import * as core from "../../../core" 2 | import * as fs from "fs" 3 | import { dumpConfState } from "./dumpConf" 4 | import * as vscode from "vscode" 5 | import * as path from "path" 6 | import * as os from "os" 7 | import { DumpConfig, prepareLogPath, runDump } from "../../../dumper/lib"; 8 | import { SqliteLogLoarder } from "../../../logLoader/sqliteLogLoader"; 9 | import { Terminal } from "../uiWrapper/terminal"; 10 | import { FileSystemPicker } from "../uiWrapper/fileSystemPicker"; 11 | import { addState, addProc, ExposeToWebview } from "../store/store"; 12 | import { StateGetter, StateGetterOf } from "../store/state"; 13 | 14 | const domain = "logFile" 15 | 16 | export const varTraceState = addState(domain, "varTrace", { 17 | userEditable: false as const, 18 | init: { 19 | varTrace: null as null | core.VarTrace 20 | }, 21 | depends: [dumpConfState] 22 | }) 23 | 24 | const getDefaultOutPath = (extensionPath: string) => { 25 | const outDir = path.join(extensionPath, ".vartrace") 26 | const outFile = path.join(outDir, "tracelog.db") 27 | return outFile 28 | } 29 | 30 | const vscodeLogLoadContext = "varTrace.logLoaded" 31 | 32 | export const initialize = (extensionPath: string) => { 33 | const proc = addProc(varTraceState, { 34 | "dump": { 35 | proc: async (get, set) => { 36 | const { varTrace } = get(varTraceState) 37 | const conf = get(dumpConfState) 38 | if (varTrace) { 39 | await varTrace.close() 40 | } 41 | const outFile = getDefaultOutPath(extensionPath) 42 | const dumperRootAbsPath = 43 | path.join(extensionPath, "dist/dumper") 44 | 45 | runDump(dumperRootAbsPath, 46 | (command: string) => Terminal.sendText(command), 47 | conf, 48 | outFile) 49 | const newVarTrace = new core.VarTrace(new SqliteLogLoarder(outFile)) 50 | set({ varTrace: newVarTrace }) 51 | vscode.commands.executeCommand('setContext', vscodeLogLoadContext, true) 52 | } 53 | }, 54 | "open": { 55 | proc: async (get, set) => { 56 | const { varTrace } = get(varTraceState) 57 | if (varTrace) { 58 | await varTrace.close() 59 | } 60 | const logPath = await FileSystemPicker.selectFile() 61 | 62 | const outFile = getDefaultOutPath(extensionPath) 63 | prepareLogPath(outFile) 64 | fs.copyFileSync(logPath, outFile) 65 | set({ 66 | varTrace: new core.VarTrace(new SqliteLogLoarder(outFile)) 67 | }) 68 | vscode.commands.executeCommand('setContext', vscodeLogLoadContext, true) 69 | } 70 | }, 71 | "save": { 72 | proc: async (_, set) => { 73 | const logPath = await FileSystemPicker.selectSavePath() 74 | const outFile = getDefaultOutPath(extensionPath) 75 | fs.copyFileSync(outFile, logPath) 76 | } 77 | } 78 | }) 79 | return proc 80 | } 81 | 82 | 83 | export type LogFile = 84 | ExposeToWebview< 85 | typeof varTraceState, 86 | ReturnType 87 | > 88 | 89 | 90 | const init = { 91 | logFileName: "none", 92 | srcFiles: [] as string[], 93 | status: "none", 94 | maxStep: 0 95 | } 96 | export const metadataState = addState(domain, "metadata", { 97 | userEditable: false as const, 98 | init, 99 | depends: [varTraceState] 100 | }) 101 | 102 | 103 | const load = async (get: StateGetterOf, set: (data: typeof init) => void) => { 104 | const currentMetadata = get(metadataState) 105 | const { varTrace } = get(varTraceState) 106 | if (varTrace === null) { 107 | return 108 | } 109 | const srcFiles = (await varTrace.getFiles()).map(x => x.absPath) 110 | const metadata = await varTrace.getMetadata() 111 | const maxStep = metadata?.maxStep ?? 0 112 | set({ 113 | srcFiles, 114 | maxStep, 115 | status: metadata?.status ?? "unknown", 116 | logFileName: currentMetadata.logFileName 117 | }) 118 | return metadata 119 | } 120 | 121 | const procs = addProc(metadataState, { 122 | "load": { 123 | proc: async (get, set) => { await load(get, set) } 124 | }, 125 | "autoReload": { 126 | triggers: [varTraceState], 127 | proc: async (get, set) => { 128 | set(init) 129 | const callBack = async () => { 130 | const medatada = await load(get, set) 131 | if (medatada?.status !== "completed") { 132 | setTimeout(callBack, 1000) 133 | } 134 | } 135 | setTimeout(callBack, 1000) 136 | } 137 | } 138 | }) 139 | 140 | 141 | 142 | export type LogMetadata = 143 | ExposeToWebview< 144 | typeof metadataState, 145 | typeof procs 146 | > 147 | -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/panelHandle.ts: -------------------------------------------------------------------------------- 1 | import { addState, updateState, addProc, ExposeToWebview } from "../store/store" 2 | import { WebviewPanel, WebviewPanelContent } from "../uiWrapper/webviewPanel" 3 | 4 | type Panels = { 5 | [key in WebviewPanelContent]: WebviewPanel 6 | } 7 | 8 | const domain = "panel" 9 | 10 | const init = { 11 | 'step detail': false, 12 | 'variable change log': false 13 | } 14 | 15 | const panelState = addState(domain, "open", { 16 | userEditable: false as const, 17 | init, 18 | depends: [] 19 | }) 20 | 21 | export const initialize = (panels: Panels) => { 22 | for (const key in panels) { 23 | panels[key as WebviewPanelContent].setOnDispose(() => { 24 | updateState(panelState, { [key]: false }) 25 | }) 26 | } 27 | 28 | const open = (key: WebviewPanelContent) => { 29 | panels[key].open() 30 | return { [key]: true } as { [key in WebviewPanelContent]: boolean } 31 | } 32 | 33 | const proc = addProc(panelState, { 34 | "openStepDetail": { 35 | proc: async (_, set) => set(open("step detail")) 36 | }, 37 | "openVarChangeLog": { 38 | proc: async (_, set) => set(open("variable change log")) 39 | } 40 | }) 41 | return proc 42 | } 43 | 44 | export type PanelHandle = 45 | ExposeToWebview< 46 | typeof panelState, 47 | ReturnType 48 | > -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/step.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StateGetter, StateGetterOf } from "../store/state" 3 | import { addProc, addState, ExposeToWebview } from "../store/store" 4 | import * as editorPanel from "../uiWrapper/editorPanel" 5 | import { varTraceState, metadataState } from "./logFile" 6 | import * as core from "../../../core" 7 | import { reloadShowOptionsState } from "./valueShowOption" 8 | import { CancelByFailure } from "./common" 9 | 10 | const domain = "step" 11 | 12 | const stepState = addState(domain, "step", { 13 | userEditable: true as const, 14 | init: { step: 0 }, 15 | depends: [metadataState, varTraceState] 16 | }) 17 | 18 | const stepProcs = addProc(stepState, { 19 | "init": { 20 | triggers: [varTraceState], 21 | proc: async (get, set) => { 22 | set({ step: 0 }) 23 | } 24 | }, 25 | "next": { 26 | proc: async (get, set) => { 27 | const logMetadata = get(metadataState) 28 | const current = get(stepState) 29 | const nextStep = Math.min( 30 | current.step + 1, 31 | logMetadata.maxStep 32 | ) 33 | set({ ...current, step: nextStep }) 34 | } 35 | }, 36 | "prev": { 37 | proc: async (get, set) => { 38 | const current = get(stepState) 39 | const prevStep = Math.max(current.step - 1, 0) 40 | set({ ...current, step: prevStep }) 41 | } 42 | } 43 | }) 44 | 45 | const stepVarFilterState = addState(domain, "filter", { 46 | userEditable: true as const, 47 | init: { 48 | varNameFilter: "" 49 | }, 50 | depends: [] 51 | }) 52 | 53 | const init = { 54 | fileName: "", 55 | line: 0, 56 | variables: {} as core.StepVars 57 | } 58 | const detailState = addState(domain, "detail", { 59 | userEditable: false as const, 60 | init, 61 | depends: [stepState, varTraceState, stepVarFilterState, reloadShowOptionsState, metadataState] 62 | }) 63 | 64 | export const initialize = () => { 65 | const load = async (get: StateGetterOf, set: (data: typeof init) => void) => { 66 | const { step } = get(stepState) 67 | const { varTrace } = get(varTraceState) 68 | const { varNameFilter } = get(stepVarFilterState) 69 | const { showIgnored, showFilterNotMatch } = get(reloadShowOptionsState) 70 | if (!varTrace) { 71 | throw new CancelByFailure("can not access analysis result") 72 | } 73 | const variables = await varTrace.getStepVars({ 74 | step, varNameFilter, showIgnored, showFilterNotMatch 75 | }) 76 | const stepInfo = await varTrace.getStepInfo(step) 77 | set({ 78 | fileName: stepInfo.fileAbsPath, 79 | line: stepInfo.line, 80 | variables, 81 | }) 82 | } 83 | const detailProcs = addProc(detailState, { 84 | "setEditorStep": { 85 | triggers: [stepState], 86 | proc: async (get, set) => { 87 | await load(get, set) 88 | const detail = get(detailState) 89 | const { fileName, line } = detail 90 | await editorPanel.show(fileName, line) 91 | } 92 | }, 93 | "load": { 94 | triggers: [metadataState, stepState, stepVarFilterState, reloadShowOptionsState], 95 | proc: async (get, set) => { 96 | await load(get, set) 97 | } 98 | } 99 | }) 100 | return detailProcs 101 | } 102 | 103 | export type Step = 104 | ExposeToWebview 105 | 106 | export type StepVarFilter = 107 | ExposeToWebview 108 | 109 | export type Detail = 110 | ExposeToWebview< 111 | typeof detailState, 112 | ReturnType 113 | > -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/valueShowOption.ts: -------------------------------------------------------------------------------- 1 | import { ProcId } from "../store/proc" 2 | import { addState, ExposeToWebview } from "../store/store" 3 | 4 | const domain = "valueShowOptions" 5 | 6 | export const noReloadShowOptionsState = addState(domain, "noReload", { 7 | userEditable: true as const, 8 | init: { 9 | multiLineText: false, 10 | showNestAsTable: false 11 | }, 12 | depends: [] 13 | }) 14 | 15 | export const reloadShowOptionsState = addState(domain, "reload", { 16 | userEditable: true as const, 17 | init: { 18 | showIgnored: true, 19 | showFilterNotMatch: false 20 | }, 21 | depends: [] 22 | }) 23 | 24 | export const initialize = () => { 25 | // dummy for webpack not to remove this module 26 | } 27 | 28 | 29 | export type NoReloadShowOptions = 30 | ExposeToWebview 31 | 32 | export type ReloadShowOptions = 33 | ExposeToWebview -------------------------------------------------------------------------------- /src/vscodeExtension/extension/processors/varChangeLog.ts: -------------------------------------------------------------------------------- 1 | import * as core from "../../../core" 2 | import { metadataState, varTraceState } from "./logFile" 3 | import { addProc, addState, ExposeToWebview } from "../store/store" 4 | import { StateGetter, StateGetterOf } from "../store/state" 5 | import * as editorPanel from "../uiWrapper/editorPanel" 6 | import { reloadShowOptionsState } from "./valueShowOption" 7 | import { CancelByFailure } from "./common" 8 | 9 | const domain = "varChangeLog" 10 | 11 | const userInputState = addState(domain, "userInput", { 12 | userEditable: true as const, 13 | init: { 14 | valueFilter: "", 15 | varNameFilter: "", 16 | scopeFilter: ["local"] as const, 17 | page: 1, 18 | pageSize: 25 19 | }, 20 | depends: [] 21 | }) 22 | 23 | const userInputProcs = addProc(userInputState, { 24 | "setVarNameFromCursor": { 25 | proc: async (get, set) => { 26 | const userInput = get(userInputState) 27 | const wordAtCursor = editorPanel.getWordAtCursor() 28 | if (!wordAtCursor) { 29 | throw new CancelByFailure("no variable found") 30 | } 31 | set({ ...userInput, varNameFilter: wordAtCursor }) 32 | } 33 | }, 34 | }) 35 | 36 | const init: core.VarChangeLog & { loading: boolean } = { 37 | maxPage: 0, 38 | page: 0, 39 | contents: [], 40 | loading: false 41 | } 42 | const resultState = addState(domain, "result", { 43 | userEditable: false as const, 44 | init, 45 | depends: [userInputState, varTraceState, metadataState, reloadShowOptionsState] 46 | }) 47 | 48 | const load = async (get: StateGetterOf, set: (data: typeof init) => void) => { 49 | const userInput = get(userInputState) 50 | const currentResult = get(resultState) 51 | const { varTrace } = get(varTraceState) 52 | const { showIgnored, showFilterNotMatch } = get(reloadShowOptionsState) 53 | set({ ...currentResult, loading: true }) 54 | if (varTrace === null) { 55 | throw new CancelByFailure("can not access analysis result") 56 | } 57 | const result = await 58 | varTrace.getVarChangeLog({ 59 | ...userInput, 60 | showIgnored, 61 | showFilterNotMatch 62 | }) 63 | set({ ...result, loading: false }) 64 | } 65 | const procs = addProc(resultState, { 66 | "init": { 67 | triggers: [varTraceState], 68 | proc: async (get, set) => { 69 | set(init) 70 | } 71 | }, 72 | "load": { 73 | triggers: [metadataState, reloadShowOptionsState], 74 | proc: load 75 | } 76 | }) 77 | 78 | export const initialize = () => { 79 | // dummy for webpack not to remove this module 80 | } 81 | 82 | 83 | export type UserInput = 84 | ExposeToWebview 85 | 86 | export type Result = 87 | ExposeToWebview 88 | 89 | -------------------------------------------------------------------------------- /src/vscodeExtension/extension/store/proc.ts: -------------------------------------------------------------------------------- 1 | import { AnyStateId, StateGetter, StateGetterOf, StateId } from "./state" 2 | 3 | export type Proc = (get: StateGetterOf, set: (data: Data) => void) => Promise 4 | export type ProcDef = 5 | Id extends StateId ? 6 | { 7 | triggers?: Depends[], 8 | proc: Proc 9 | } 10 | : never 11 | export type ProcDefs = 12 | Record> 13 | 14 | export type ProcId = 15 | `${SId}/${Name}` & Partial<{ stateId: SId, name: Name }> 16 | 17 | export type AnyProcId = ProcId 18 | 19 | export const makeProcId = ( 20 | stateId: SId, 21 | name: Name 22 | ) => { 23 | return `${stateId}/${name}` as ProcId 24 | } 25 | -------------------------------------------------------------------------------- /src/vscodeExtension/extension/store/state.ts: -------------------------------------------------------------------------------- 1 | 2 | export type StateDef = 3 | Data extends Array ? never 4 | : 5 | UserEditable extends boolean ? 6 | { 7 | init: Data, 8 | userEditable: UserEditable, 9 | depends: Depends[] 10 | } : never 11 | 12 | export type StateId = 13 | Domain extends `${infer _Prefix}/${infer _Suffix}` ? never : 14 | Name extends `${infer _Prefix}/${infer _Suffix}` ? never : 15 | `${Domain}/${Name}` & Partial<{ 16 | domain: Domain, 17 | name: Name, 18 | data: Data, 19 | userEditable: UserEditable, 20 | stateKey: true, 21 | depends: Depends 22 | }> 23 | 24 | export type StateGetter = (key: StateId) => 25 | StateDataOf 26 | 27 | export type StateGetterOf = 28 | S extends StateId ? 29 | StateGetter : never 30 | 31 | export type StateDataOf = 32 | S extends StateId ? 33 | Data : never 34 | 35 | export type AnyEditableStateId = StateId 36 | 37 | export type AnyStateId = StateId 38 | 39 | export const makeStateId = ( 40 | domain: Domain, name: Name, def: StateDef) => { 41 | return `${domain}/${name}` as StateId 42 | } 43 | -------------------------------------------------------------------------------- /src/vscodeExtension/extension/store/store.ts: -------------------------------------------------------------------------------- 1 | import { AnyProcId, makeProcId, Proc, ProcDefs, ProcId } from "./proc" 2 | import { AnyStateId, makeStateId, StateDataOf, StateDef, StateGetter, StateId } from "./state" 3 | 4 | export type ExposeToWebview = 5 | SId extends AnyStateId ? 6 | Procs extends (ProcId)[] ? 7 | { 8 | procs: Names 9 | stateId: SId 10 | } 11 | : never 12 | : never 13 | 14 | export type StateIdOf = 15 | Store extends { procs: infer Names, stateId: infer SId } ? 16 | SId : never 17 | 18 | export type ProcNameOf = 19 | Store extends { procs: infer Names, stateId: infer SId } ? 20 | Names : never 21 | 22 | type ProcEntry = { stateId: string, proc: Proc } 23 | const stateData: { [stateId in AnyStateId]: any } = {} 24 | const processors: { [procId in AnyProcId]: ProcEntry } = {} 25 | const triggerCalls: { [triggerStateId in AnyStateId]: ProcEntry[] } = {} 26 | const subscribers: { 27 | [stateId: AnyStateId]: { 28 | [subscriberName: string]: (data: any) => void 29 | } 30 | } = {} 31 | 32 | export const addState = ( 33 | domain: Domain, name: Name, def: StateDef) 34 | : StateId => { 35 | const stateId = makeStateId(domain, name, def) 36 | stateData[stateId] = def.init 37 | triggerCalls[stateId] = [] 38 | publish(stateId) 39 | return stateId 40 | } 41 | 42 | export const addProc = ( 43 | stateId: SId, 44 | procs: ProcDefs 45 | ) 46 | : ProcId[] => { 47 | 48 | let result: ProcId[] = [] 49 | for (const procName in procs) { 50 | if (procs[procName] === undefined) { 51 | continue 52 | } 53 | const { proc, triggers } = procs[procName] 54 | const procId = makeProcId(stateId, procName as ProcKeys) 55 | processors[procId] = { 56 | stateId, 57 | proc 58 | } 59 | for (const triggerStateId of triggers ?? []) { 60 | triggerCalls[triggerStateId]!.push({ 61 | stateId, 62 | proc 63 | }) 64 | } 65 | result.push(procId) 66 | } 67 | return result 68 | } 69 | 70 | 71 | export const addSubscriber = ( 72 | stateId: AnyStateId, 73 | subscriberName: string, 74 | subscriber: (get: StateGetter) => void) => { 75 | const newSubscription = { 76 | ...subscribers[stateId], 77 | [subscriberName]: subscriber 78 | } 79 | subscribers[stateId] = newSubscription 80 | publish(stateId) 81 | } 82 | 83 | const publish = (stateId: AnyStateId) => { 84 | const data = stateData[stateId] 85 | for (const subscriber of Object.values(subscribers[stateId] ?? {})) { 86 | subscriber(data) 87 | } 88 | } 89 | 90 | const fireTrigger = (stateId: AnyStateId) => { 91 | for (const processor of triggerCalls[stateId] ?? []) { 92 | processor.proc( 93 | (stateId: any) => stateData[stateId], 94 | (data: any) => updateState(processor.stateId, data) 95 | ) 96 | } 97 | } 98 | 99 | export const updateState = (stateId: SId, data: Partial>) => { 100 | stateData[stateId] = { ...stateData[stateId], ...data } 101 | fireTrigger(stateId) 102 | publish(stateId) 103 | } 104 | 105 | export const callProc = async (stateId: StateIdOf, procName: ProcNameOf) => { 106 | const procId = makeProcId(stateId as AnyStateId, procName as string) 107 | const processor = processors[procId] 108 | if (processor === undefined) { 109 | throw new Error("unregisterd proc call") 110 | } 111 | 112 | await processor.proc( 113 | (key: any) => stateData[key], 114 | (data: any) => updateState(processor.stateId, data) 115 | ) 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/vscodeExtension/extension/uiWrapper/editorPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | 4 | const highlight = (editor: vscode.TextEditor, line: number) => { 5 | // vscode display is 1-based. but vscode api treat line 0-based 6 | const anchorPos = new vscode.Position(line - 1, 0) 7 | const activePos = new vscode.Position(line, 0) 8 | const selection = new vscode.Selection(anchorPos, activePos) 9 | editor.selection = selection 10 | } 11 | 12 | const scroll = (editor: vscode.TextEditor, line: number) => { 13 | // vscode display is 1-based. but vscode api treat line 0-based 14 | const range = new vscode.Range(line - 1, 0, line, 0) 15 | editor.revealRange(range, vscode.TextEditorRevealType.InCenterIfOutsideViewport) 16 | } 17 | export const getWordAtCursor = () => { 18 | const currentEditor = vscode.window.activeTextEditor 19 | if (!currentEditor) { 20 | return undefined 21 | } 22 | const range = currentEditor.document.getWordRangeAtPosition( 23 | currentEditor.selection.active 24 | ); 25 | return range ? currentEditor.document.getText(range) : undefined 26 | } 27 | 28 | export const getCurrentState = () => { 29 | const currentEditor = vscode.window.activeTextEditor 30 | if (!currentEditor) { 31 | return undefined 32 | } 33 | const file = currentEditor.document.fileName 34 | // vscode display is 1-based. but vscode api treat line 0-based 35 | const line = currentEditor.selection.active.line + 1 36 | return { file, line } 37 | } 38 | const openEditor = async (fileName: string) => { 39 | const doc = await vscode.workspace.openTextDocument(fileName) 40 | const editor = await vscode.window.showTextDocument(doc, vscode.ViewColumn.One, false) 41 | return editor 42 | } 43 | 44 | export const show = async (fileName: string, line: number) => { 45 | const editor = await openEditor(fileName) 46 | highlight(editor, line) 47 | scroll(editor, line) 48 | } 49 | 50 | export const getBreakPoints = () => { 51 | const rawBreakPoints = vscode.debug.breakpoints 52 | const result: { [fileAbsPath: string]: number[] } = {} 53 | for (const point of rawBreakPoints) { 54 | if (!(point instanceof vscode.SourceBreakpoint)) { 55 | continue 56 | } 57 | const { uri, range } = point.location 58 | const fileAbsPath = uri.fsPath 59 | // in vscode api, line is 0-based. 60 | const line = range.start.line + 1 61 | result[fileAbsPath] ??= [] 62 | result[fileAbsPath]?.push(line) 63 | } 64 | return result 65 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/uiWrapper/fileSystemPicker.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class FileSystemPicker { 4 | private static defaultConf: vscode.OpenDialogOptions = { 5 | canSelectMany: false, 6 | openLabel: 'Open', 7 | filters: { 8 | 'All files': ['*'] 9 | } 10 | }; 11 | static selectFile = async () => { 12 | const options = { 13 | ...FileSystemPicker.defaultConf, 14 | canSelectFolders: false, 15 | canSelectFiles: true, 16 | } 17 | const fileUri = await vscode.window.showOpenDialog(options) 18 | const path = fileUri && fileUri[0]?.fsPath 19 | if (path === undefined) { 20 | throw Error("path is undefined") 21 | } 22 | return path 23 | } 24 | static selectDir = async () => { 25 | const options = { 26 | ...FileSystemPicker.defaultConf, 27 | canSelectFolders: true, 28 | canSelectFiles: false, 29 | } 30 | const fileUri = await vscode.window.showOpenDialog(options) 31 | const path = fileUri && fileUri[0]?.fsPath 32 | if (path === undefined) { 33 | throw Error("path is undefined") 34 | } 35 | return path 36 | } 37 | static selectSavePath = async () => { 38 | const fileUri = await vscode.window.showSaveDialog({}) 39 | if (fileUri === undefined) { 40 | throw Error("path is undefined") 41 | } 42 | return fileUri.fsPath 43 | } 44 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/uiWrapper/notification.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export const inform = (message: string) => { 4 | vscode.window.showInformationMessage(message); 5 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/uiWrapper/terminal.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class Terminal { 4 | private static terminalName = `Vartrace` 5 | private static openMessage = ` 6 | ########################### \r\n 7 | VarTrace \r\n 8 | ########################### \r\n 9 | executing your script... 10 | 11 | ` 12 | private static getTerminal = () => { 13 | let terminal = vscode.window.terminals.find(x => x.name === this.terminalName) 14 | if (terminal !== undefined) { 15 | const ctrC = '\x03' 16 | // make sure there is no command. (user might run a command on the terminal) 17 | terminal.sendText(ctrC + ctrC) 18 | } 19 | terminal ??= vscode.window.createTerminal({ 20 | name: this.terminalName, 21 | message: this.openMessage 22 | } as any) 23 | return terminal 24 | } 25 | static sendText = (text: string) => { 26 | const terminal = this.getTerminal() 27 | terminal.show(); 28 | terminal.sendText(text) 29 | } 30 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/uiWrapper/webviewPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { handleMessage } from "../messageHandler"; 4 | 5 | export type WebviewPanelContent = 6 | "step detail" 7 | | "variable change log" 8 | 9 | 10 | export class WebviewPanel { 11 | private panel: vscode.WebviewPanel | undefined; 12 | private reactAppUri: vscode.Uri; 13 | private isPanelDisposed = false 14 | private onDispose = () => { } 15 | 16 | constructor( 17 | public key: WebviewPanelContent, 18 | private context: vscode.ExtensionContext) { 19 | 20 | // Local path to main script run in the webview 21 | const reactAppPathOnDisk = vscode.Uri.file( 22 | path.join(context.extensionPath, "dist", "webview.js") 23 | ); 24 | this.reactAppUri = reactAppPathOnDisk.with({ scheme: "vscode-resource" }); 25 | 26 | } 27 | 28 | setOnDispose = (onDispose: () => void) => { 29 | this.onDispose = onDispose 30 | } 31 | 32 | createPanel = () => { 33 | const extensionPath = this.context.extensionPath 34 | this.panel = vscode.window.createWebviewPanel( 35 | this.key, 36 | this.key, 37 | { preserveFocus: true, viewColumn: vscode.ViewColumn.Beside }, 38 | { 39 | enableScripts: true, 40 | retainContextWhenHidden: true, 41 | localResourceRoots: [ 42 | vscode.Uri.file(path.join(extensionPath)) 43 | ] 44 | } 45 | ); 46 | 47 | this.panel.webview.onDidReceiveMessage( 48 | message => handleMessage(message, this)) 49 | this.panel.onDidDispose(() => { 50 | this.isPanelDisposed = true 51 | this.onDispose() 52 | }) 53 | this.panel.webview.html = this.createWebViewHtmlString() 54 | this.panel.iconPath = vscode.Uri.joinPath(vscode.Uri.file(extensionPath), "resources", "vt.png") 55 | 56 | } 57 | 58 | open = () => { 59 | if (this.isPanelDisposed || this.panel === undefined) { 60 | this.createPanel() 61 | this.isPanelDisposed = false 62 | } 63 | if (!this.panel?.visible) { 64 | this.panel?.reveal() 65 | } 66 | } 67 | 68 | private createWebViewHtmlString = () => { 69 | 70 | return ` 71 | 72 | 73 | 74 | 75 | Vartrace Panel 76 | 81 | 85 | 86 | 87 |
88 | 89 | 90 | `; 91 | } 92 | 93 | post = (message: any) => { 94 | try { 95 | this.panel?.webview.postMessage(message) 96 | } catch (e) { 97 | // TODO: error handling 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/vscodeExtension/extension/uiWrapper/webviewView.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { handleMessage } from "../messageHandler"; 4 | 5 | export type WebviewViewContent = "sidebar" 6 | 7 | 8 | export class WebviewViewProvider implements vscode.WebviewViewProvider { 9 | private reactAppUri: vscode.Uri; 10 | private webviewView: vscode.WebviewView | undefined = undefined; 11 | 12 | constructor( 13 | public key: WebviewViewContent, 14 | private readonly context: vscode.ExtensionContext, 15 | ) { 16 | 17 | const reactAppPathOnDisk = vscode.Uri.file( 18 | path.join(this.context.extensionPath, "dist", "webview.js") 19 | ); 20 | this.reactAppUri = reactAppPathOnDisk.with({ scheme: "vscode-resource" }); 21 | 22 | this.context.subscriptions.push( 23 | vscode.window.registerWebviewViewProvider( 24 | `vartrace.${key}`, // should be same as package.json webview id 25 | this, { 26 | webviewOptions: { 27 | retainContextWhenHidden: true 28 | } 29 | }) 30 | ); 31 | } 32 | 33 | public resolveWebviewView( 34 | webviewView: vscode.WebviewView, 35 | context: vscode.WebviewViewResolveContext, 36 | _token: vscode.CancellationToken, 37 | ) { 38 | webviewView.webview.options = { 39 | enableScripts: true, 40 | localResourceRoots: [ 41 | vscode.Uri.file(path.join(this.context.extensionPath)) 42 | ] 43 | }; 44 | 45 | 46 | webviewView.webview.html = this.createWebViewHtmlString(); 47 | this.webviewView = webviewView 48 | this.webviewView.webview.onDidReceiveMessage( 49 | message => handleMessage(message, this)) 50 | } 51 | 52 | post = (message: any) => { 53 | this.webviewView?.webview.postMessage(message) 54 | } 55 | 56 | private createWebViewHtmlString = () => { 57 | return ` 58 | 59 | 60 | 61 | 62 | Vartrace View 63 | 68 | 72 | 73 | 74 |
75 | 76 | 77 | `; 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/vscodeExtension/messaging.ts: -------------------------------------------------------------------------------- 1 | import { AnyEditableStateId, AnyStateId, StateDataOf } from "./extension/store/state" 2 | import { ExposeToWebview, ProcNameOf, StateIdOf } from "./extension/store/store" 3 | 4 | type SubscribeMessage = { 5 | type: "subscribe", 6 | stateId: AnyStateId, 7 | } 8 | 9 | type UpdateMessage = { 10 | type: "update", 11 | stateId: StateId, 12 | data: Partial> 13 | } 14 | type CallMessage = { 15 | type: "call", 16 | stateId: StateIdOf, 17 | procName: ProcNameOf, 18 | } 19 | 20 | type AnyCallMessage = { 21 | type: "call", 22 | stateId: AnyStateId, 23 | procName: string, 24 | } 25 | 26 | type DisplayMessage = { 27 | type: "display", 28 | stateId: StateId, 29 | data: StateDataOf 30 | } 31 | 32 | export const encodeSubscribeMessage = 33 | (stateId: StateId): SubscribeMessage => { 34 | return { type: "subscribe", stateId } 35 | } 36 | 37 | export const encodeUpdateMessage = 38 | (stateId: StateId, data: StateDataOf) 39 | : UpdateMessage => { 40 | return { type: "update", stateId, data } 41 | } 42 | 43 | export const encodeCallMessage = (stateId: StateIdOf, procName: ProcNameOf) 44 | : CallMessage => { 45 | return { type: "call", stateId, procName } 46 | } 47 | 48 | export const isCallMessage = (message: any): message is AnyCallMessage => { 49 | const assumed: Partial> = message 50 | return assumed.type === "call" 51 | && typeof assumed.stateId === "string" 52 | && typeof assumed.procName === "string" 53 | } 54 | 55 | export const isSubscribeMessage = (message: any): message is SubscribeMessage => { 56 | const assumed: Partial = message 57 | return assumed.type === "subscribe" 58 | && typeof assumed.stateId === "string" 59 | } 60 | 61 | export const isUpdateMessage = (message: any): message is UpdateMessage => { 62 | const assumed: Partial> = message 63 | return assumed.type === "update" 64 | && typeof assumed.stateId === "string" 65 | && typeof assumed.data === "object" 66 | } 67 | 68 | export const encodeDisplayMessage = ( 69 | stateId: SId, data: StateDataOf): DisplayMessage => { 70 | return { type: "display", stateId, data } 71 | } 72 | 73 | export const tryDecodeDisplayMessage = 74 | (stateId: SId, message: any): StateDataOf => { 75 | const assumed: Partial> = message 76 | const bad_conditions = [ 77 | typeof assumed !== "object", 78 | assumed.type !== "display", 79 | assumed.stateId !== stateId, 80 | message.data === undefined 81 | ] 82 | if (bad_conditions.some(x => x)) { 83 | throw new Error(`failed to decode as ${stateId}`) 84 | } 85 | return message.data 86 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/accessor.ts: -------------------------------------------------------------------------------- 1 | import { makeAccessor } from "./webviewMessaging" 2 | 3 | 4 | 5 | import { DumpConf } from "../extension/processors/dumpConf" 6 | export const dumpConf = makeAccessor("dumpConf/userInput") 7 | 8 | import { LogFile, LogMetadata } from "../extension/processors/logFile"; 9 | export const logFile = makeAccessor("logFile/varTrace") 10 | export const logMetadata = makeAccessor("logFile/metadata") 11 | 12 | import { PanelHandle } from "../extension/processors/panelHandle"; 13 | export const panelOpen = makeAccessor("panel/open") 14 | 15 | import { ReloadShowOptions, NoReloadShowOptions } from "../extension/processors/valueShowOption"; 16 | export const reloadShowOptions = 17 | makeAccessor("valueShowOptions/reload") 18 | export const noReloadShowOptions = 19 | makeAccessor("valueShowOptions/noReload") 20 | 21 | import { Detail, Step, StepVarFilter } from "../extension/processors/step"; 22 | export const step = makeAccessor("step/step") 23 | export const stepVarFilter = makeAccessor("step/filter") 24 | export const stepDetail = makeAccessor("step/detail") 25 | 26 | import { BreakPointSteps } 27 | from "../extension/processors/breakPoint" 28 | export const breakPointSteps = makeAccessor("breakPoints/breakPointSteps") 29 | 30 | 31 | import { Result as VarChangeLogResult, UserInput as VarChangeLogUserInput } 32 | from "../extension/processors/varChangeLog" 33 | export const varChangeLogUserInput = makeAccessor("varChangeLog/userInput") 34 | export const varChangeLogResult = makeAccessor("varChangeLog/result") -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/BreakPointSteps.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Accordion, AccordionDetails, AccordionSummary, Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from "@mui/material" 3 | import { breakPointSteps } from "../accessor"; 4 | import { StepLink } from "../components/StepLink"; 5 | import { ExpandMore } from "@mui/icons-material"; 6 | 7 | const TableHeader = () => { 8 | return 9 | 10 | line 11 | steps 12 | 13 | 14 | } 15 | 16 | const Row = (props: { steps: number[], line: string }) => { 17 | const { steps, line } = props 18 | return 19 | 20 | 21 | {line} 22 | 23 | 24 | 25 | { 26 | steps.map(step => 27 | 28 | 29 | 30 | )} 31 | 32 | 33 | 34 | } 35 | 36 | const FilePath = (props: { path: string }) => { 37 | const separator = /[\\,\/]/ 38 | const path = props.path.split(separator) 39 | const fileName = path[path.length - 1] 40 | return 46 | {fileName} 47 | 48 | } 49 | 50 | 51 | export const BreakPointSteps = () => { 52 | const breaks = breakPointSteps.useData()?.breakPointSteps 53 | 54 | if (!breaks) { 55 | return <> 56 | } 57 | 58 | return <> 59 | 60 | } 62 | sx={{ minHeight: "36px" }} 63 | > 64 | breakpoint steps 65 | 66 | 67 | { 68 | Object.entries(breaks).map(([fileAbsPath, lineSteps]) => 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {Object.entries(lineSteps).map(([line, steps]) => 78 | 79 | )} 80 | 81 |
82 |
83 | 84 |
) 85 | } 86 |
87 |
88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/CircularBackdrop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Backdrop, CircularProgress } from "@mui/material" 3 | 4 | export const CircularBackdrop = (props: { open: boolean }) => { 5 | return theme.zIndex.drawer + 1 }} 7 | open={props.open} 8 | > 9 | 10 | 11 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/HighlightedTree.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useId } from "react-id-generator"; 3 | import { Box, styled, Theme, useTheme } from "@mui/material"; 4 | import * as core from "../../../core" 5 | import { SxProps } from "@mui/system"; 6 | import { TreeItem, TreeView } from "@mui/lab"; 7 | import { ExpandMore, ChevronRight } from "@mui/icons-material"; 8 | import { noReloadShowOptions, reloadShowOptions } from "../accessor"; 9 | 10 | type TextViewModel = { 11 | text: string, 12 | isHighlight: boolean, 13 | isBold: boolean, 14 | } 15 | 16 | const toViewModels = (valueText: core.ValueText): TextViewModel[] => { 17 | let cur = 0 18 | let result = [] 19 | const isBold = Boolean(valueText.isNew || valueText.isChanged) 20 | for (const { start, end } of valueText.hit) { 21 | result.push({ 22 | text: valueText.text.substring(cur, start), 23 | isBold, 24 | isHighlight: false 25 | }) 26 | result.push({ 27 | text: valueText.text.substring(start, end), 28 | isBold, 29 | isHighlight: true 30 | }) 31 | cur = end 32 | } 33 | result.push({ 34 | text: valueText.text.substring(cur), 35 | isBold, 36 | isHighlight: false 37 | }) 38 | return result 39 | } 40 | 41 | const splitByChar = (viewModel: TextViewModel): TextViewModel[] => { 42 | return [...viewModel.text].map(char => ({ 43 | text: char, 44 | isBold: viewModel.isBold, 45 | isHighlight: viewModel.isHighlight 46 | })) 47 | } 48 | 49 | const toStyle = (theme: Theme, viewModel: TextViewModel) => { 50 | let style: SxProps = { hyphens: "none", whiteSpace: "nowrap" } 51 | if (viewModel.isBold) { 52 | style = { 53 | ...style, 54 | fontWeight: 'bold', 55 | color: theme.palette.primary.main, 56 | } 57 | } 58 | if (viewModel.isHighlight) { 59 | style = { 60 | ...style, 61 | backgroundColor: theme.palette.secondary.contrastText + "77" // add transparent 62 | } 63 | } 64 | // '\n' doesnt appear without restoreEscape called 65 | if (viewModel.text === '\n') { 66 | style = { ...style, flexBasis: "100%", height: "0" } 67 | } 68 | return style 69 | } 70 | 71 | const restoreEscape = (viewModel: TextViewModel) => { 72 | const escapes = { 73 | '\\n': '\n', 74 | '\\b': '\b', 75 | '\\t': '\t', 76 | '\\f': '\f', 77 | '\\r': '\r', 78 | '\\a': '\a', 79 | '\\"': '"', 80 | '\\/': '/', 81 | } 82 | let text = viewModel.text 83 | 84 | for (let [escape, restore] of Object.entries(escapes)) { 85 | text = text.replaceAll(escape, restore) 86 | } 87 | 88 | return { 89 | ...viewModel, 90 | text 91 | } 92 | } 93 | 94 | const convertAsText = (decoratedTexts: core.ValueText[]): TextViewModel[] => { 95 | let result = [] 96 | for (let decoratedText of decoratedTexts) { 97 | let viewModels = toViewModels(decoratedText) 98 | viewModels = viewModels.map(restoreEscape) 99 | viewModels = viewModels.flatMap(splitByChar) 100 | result.push(...viewModels) 101 | } 102 | // replace first " and end " 103 | return result.slice(1, result.length - 1) 104 | } 105 | 106 | export const HighlightedSpans = (props: { valueTexts: core.ValueText[], asText: boolean }) => { 107 | const theme = useTheme() 108 | const chars = props.asText 109 | ? convertAsText(props.valueTexts) 110 | : props.valueTexts.flatMap(toViewModels) 111 | 112 | const content = <> 113 | { 114 | chars.map( 115 | (x) => 118 | {x.text.replaceAll(' ', '\u00A0') /* space(html will remove) -> nbsp */} 119 | 120 | ) 121 | } 122 | 123 | return props.asText ? {content} : content 124 | } 125 | 126 | const getDim = (value: core.Value) => { 127 | let maxDim = 0 128 | for (const child of Object.values(value.children)) { 129 | maxDim = Math.max(maxDim, getDim(child.value) + 1) 130 | } 131 | return maxDim 132 | } 133 | 134 | const TableRoot = styled('div')` 135 | table { 136 | border-collapse: collapse; 137 | width: 100%; 138 | } 139 | 140 | td, th { 141 | border: 1px solid ${props => props.theme.palette.action.disabled}; 142 | } 143 | th { 144 | background-color: ${props => props.theme.palette.action.disabled}; 145 | } 146 | `; 147 | 148 | 149 | export const Table2d = (props: { value: core.Value }) => { 150 | const { value } = props 151 | const dict: { [key: string]: { [key: string]: core.ValueText[] } } = {} 152 | const colKeySet = new Set() 153 | for (const [childKey, child] of Object.entries(value.children)) { 154 | dict[childKey] = {} 155 | for (const [grandChildKey, grandChild] of Object.entries(child.value.children)) { 156 | dict[childKey] = { 157 | ...dict[childKey], 158 | [grandChildKey]: grandChild.value.expression 159 | } 160 | colKeySet.add(grandChildKey) 161 | } 162 | } 163 | // sort func return NaN if a or b is not number string 164 | const rowKeys = Object.keys(dict).sort((a: any, b: any) => a - b) 165 | const colKeys = [...colKeySet].sort((a: any, b: any) => a - b) 166 | 167 | return 168 | 169 | 170 | 171 | {colKeys.map(colKey => )} 172 | 173 | { 174 | rowKeys.map(rowKey => 175 | 176 | {colKeys.map(colKey => )} 179 | ) 180 | } 181 |
{colKey}
{rowKey} 177 | 178 |
182 |
183 | } 184 | export const Table1d = (props: { value: core.Value }) => { 185 | const { value } = props 186 | const colKeySet = new Set() 187 | for (const [key, child] of Object.entries(value.children)) { 188 | colKeySet.add(key) 189 | } 190 | // note: sort func return NaN if a or b is not number string 191 | // this doesnt cause any matter now. but in the future might cause 192 | const colKeys = [...colKeySet].sort((a: any, b: any) => a - b) 193 | 194 | return 195 | 196 | 197 | {colKeys.map(colKey => )} 198 | 199 | 200 | { 201 | Object.values(value.children).map(child => 202 | 205 | ) 206 | } 207 | 208 |
{colKey}
203 | 204 |
209 |
210 | } 211 | type ShowType = "none" | "string" | "table2d" | "table1d" 212 | 213 | const getShowType = (value: core.Value) => { 214 | const noReloadOptions = noReloadShowOptions.useData() 215 | const reloadOptions = reloadShowOptions.useData() 216 | const options = noReloadOptions && reloadOptions && 217 | { ...reloadOptions, ...noReloadOptions } 218 | let showType: ShowType = "none" 219 | if (!options) { 220 | return showType 221 | } 222 | if (value.type === "string" && options.multiLineText) { 223 | showType = "string" 224 | } 225 | if (options.showNestAsTable) { 226 | const dim = getDim(value) 227 | if (dim == 2) { 228 | showType = "table2d" 229 | } 230 | else if (dim == 1) { 231 | showType = "table1d" 232 | } 233 | } 234 | 235 | return showType 236 | } 237 | 238 | export const HighlightedTreeLabel = (props: { keyExpression: core.ValueText[], value: core.Value, showType: ShowType }) => { 239 | const { value, showType, keyExpression } = props 240 | 241 | return 242 | 243 | {(keyExpression.length ?? 0) > 0 && : } 244 | 245 | {showType === "string" && 246 | 247 | } 248 | {showType === "none" && 249 | 250 | } 251 | { 252 | showType === "table2d" && 253 | 254 | } 255 | { 256 | showType === "table1d" && 257 | 258 | } 259 | 260 | } 261 | 262 | export const HighlightedTreeItem = (props: { value: core.Value, keyExpression: core.ValueText[] }) => { 263 | const { value, keyExpression } = props 264 | const showType = getShowType(value) 265 | 266 | const theme = useTheme() 267 | const [id] = useId() 268 | const hasHitStyle = { 269 | fill: theme.palette.secondary.main, 270 | color: theme.palette.secondary.main, 271 | } 272 | const style = value.hasHit ? hasHitStyle : {} 273 | return } 276 | endIcon={value.hasHit ? :
} 277 | collapseIcon={} 278 | expandIcon={} 279 | > 280 | {(!["table2d", "table1d"].includes(showType)) && Object.entries(value.children).map( 281 | ([key, { value, keyExpression }]) => 282 | 283 | )} 284 |
285 | } 286 | 287 | export const HighlightedTree = (props: { value: core.Value }) => { 288 | 289 | return } 291 | defaultExpandIcon={} 292 | disableSelection 293 | > 294 | 295 | 296 | } 297 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from "@mui/icons-material"; 2 | import { IconButton, InputAdornment, TextField } from "@mui/material"; 3 | import * as React from "react"; 4 | 5 | type Props = { 6 | label?: string, 7 | onConfirm?: () => void, 8 | onChange: (text: string) => void, 9 | placeholder: string, 10 | value: string 11 | } 12 | 13 | export const SearchBar: React.FC = (props) => { 14 | const { onConfirm, placeholder, value, onChange, label } = props 15 | const [input, setInput] = React.useState(value) 16 | React.useEffect(() => { 17 | setInput(value) 18 | }, [value]) 19 | return <>{ 20 | { 28 | // workaround of https://github.com/mui/material-ui/issues/4430 29 | setInput(event.target.value) 30 | onChange(event.target.value) 31 | }} 32 | onKeyDown={event => { 33 | // note: https://qiita.com/ledsun/items/31e43a97413dd3c8e38e 34 | if (event.keyCode === 13) { 35 | onConfirm && onConfirm() 36 | } 37 | }} 38 | InputProps={{ 39 | endAdornment: ( 40 | onConfirm && 41 | { 44 | onConfirm() 45 | }}> 46 | {} 47 | 48 | 49 | ), 50 | style: { paddingRight: 0 } 51 | }} 52 | />} 53 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/SelectList.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; 3 | import * as React from "react"; 4 | 5 | type OneProps = { 6 | label?: string, 7 | value: T, 8 | list: T[], 9 | onChange: (arg: T) => void 10 | } 11 | 12 | export const SelectOneList = (props: OneProps) => { 13 | const { label, onChange, value, list } = props 14 | return 15 | {label && {label}} 16 | 31 | 32 | } 33 | 34 | 35 | type MultipleProps = { 36 | label?: string, 37 | value: T[], 38 | list: T[], 39 | onChange: (arg: T[]) => void 40 | } 41 | export const SelectMultipleList = (props: MultipleProps) => { 42 | const { onChange, value, list, label } = props 43 | return 44 | {label && {label}} 45 | 62 | 63 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/StepLink.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Link } from "@mui/material" 2 | import * as React from "react" 3 | import { step } from "../accessor" 4 | 5 | 6 | export const StepLink = (props: { step: number }) => { 7 | return 8 | step.sendEdit({ step: props.step }) 14 | } 15 | > 16 | {props.step} 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/components/ValueShowSettings.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react"; 3 | import { Settings } from "@mui/icons-material"; 4 | import { Box, Checkbox, FormControlLabel, IconButton, Menu, styled } from "@mui/material"; 5 | import { noReloadShowOptions, reloadShowOptions } from "../accessor"; 6 | 7 | const OutlinedIconButton = styled(IconButton)` 8 | border: 1px solid ${props => props.theme.palette.action.disabled}; 9 | border-radius: 5px; 10 | `; 11 | 12 | export const ValueShowSettings = () => { 13 | const reloadOptions = reloadShowOptions.useData() 14 | const noReloadOptions = noReloadShowOptions.useData() 15 | const [anchorEl, setAnchorEl] = React.useState(null); 16 | if (reloadOptions === undefined || noReloadOptions === undefined) { 17 | return <> 18 | } 19 | return <> 20 | setAnchorEl(null)} 26 | > 27 | 28 | 29 | reloadShowOptions.sendEdit({ 34 | showFilterNotMatch: !reloadOptions.showFilterNotMatch 35 | })} 36 | />} 37 | /> 38 | reloadShowOptions.sendEdit({ 43 | showIgnored: !reloadOptions.showIgnored 44 | })} 45 | />} /> 46 | 47 | noReloadShowOptions.sendEdit({ 52 | showNestAsTable: !noReloadOptions.showNestAsTable 53 | })} 54 | />} 55 | /> 56 | noReloadShowOptions.sendEdit({ 61 | multiLineText: !noReloadOptions.multiLineText 62 | })} 63 | />} 64 | /> 65 | 66 | 67 | 68 | 69 | setAnchorEl(event.currentTarget)}> 72 | 73 | 74 | 75 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import { ThemeProvider } from "@mui/material/styles"; 5 | import { StepDetailPanel } from "./panel/StepDetailPanel"; 6 | import { useGeneratedTheme } from "./webviewTheme"; 7 | import { VarChangeLogPanel } from "./panel/VarChangeLogPanel"; 8 | 9 | import { Sidebar } from "./sidebar/Sidebar"; 10 | import { CssBaseline } from "@mui/material"; 11 | import { WebviewPanelContent } from "../extension/uiWrapper/webviewPanel"; 12 | import { WebviewViewContent } from "../extension/uiWrapper/webviewView"; 13 | 14 | 15 | declare global { 16 | var content: WebviewPanelContent | WebviewViewContent 17 | } 18 | 19 | const Webview = () => { 20 | switch (content) { 21 | case "step detail": return 22 | case "variable change log": return 23 | case "sidebar": return 24 | // dummy to treat non exhausive switch-case as an error 25 | // https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript 26 | default: return ((never: never) => <>)(content) 27 | } 28 | } 29 | 30 | const getThemeName = () => { 31 | if (content === "sidebar") return "sideBar" as const 32 | return "panel" as const 33 | } 34 | 35 | const Main = () => { 36 | const themeName = getThemeName() 37 | const theme = useGeneratedTheme(themeName) 38 | return 39 | 40 | 41 | 42 | } 43 | 44 | ReactDOM.render( 45 |
, 46 | document.getElementById("root") 47 | ); 48 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/panel/StepDetailPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as core from "../../../core" 3 | import { HighlightedSpans, HighlightedTree } from "../components/HighlightedTree"; 4 | import { Box, Divider, Grid, IconButton, Paper, Slider, styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" 5 | import { ArrowLeft, ArrowRight } from "@mui/icons-material"; 6 | import { ValueShowSettings } from "../components/ValueShowSettings"; 7 | import { step, stepDetail, logMetadata, breakPointSteps, stepVarFilter, noReloadShowOptions, reloadShowOptions } from "../accessor"; 8 | import { SearchBar } from "../components/SearchBar"; 9 | import { BreakPointSteps } from "../components/BreakPointSteps"; 10 | 11 | type Variables = { 12 | varName: core.ValueText[], 13 | before: core.Value | undefined, 14 | after: core.Value | undefined 15 | }[] 16 | 17 | const OutlinedIconButton = styled(IconButton)` 18 | border: 1px solid ${props => props.theme.palette.action.disabled}; 19 | border-radius: 5px; 20 | `; 21 | 22 | 23 | 24 | const StepControllButtons = () => { 25 | return 26 | step.callProc("prev")}> 29 | 30 | 31 | step.callProc("next")}> 34 | 35 | 36 | 37 | } 38 | 39 | 40 | const StepControllBar = () => { 41 | const logFile = logMetadata.useData() 42 | 43 | const userInput = step.useData() 44 | const [inputStep, setInputStep] = React.useState(userInput?.step) 45 | 46 | React.useEffect(() => setInputStep(userInput?.step), [userInput?.step]) 47 | return setInputStep(v as number)} 54 | onChangeCommitted={(_e, v) => step.sendEdit({ step: v as number })} 55 | valueLabelDisplay="auto" 56 | /> 57 | } 58 | 59 | const StepControll = () => { 60 | const userInput = step.useData() 61 | const detail = stepDetail.useData() 62 | return <> 63 | 64 | 65 | step control 66 | 67 | step: {userInput?.step} line: {detail?.line} 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | } 80 | 81 | 82 | const VarTableHeader = () => { 83 | return 84 | 85 | variable 86 | before 87 | after 88 | 89 | 90 | } 91 | 92 | const SearchVarName = () => { 93 | const filter = stepVarFilter.useData() 94 | return 95 | stepVarFilter.sendEdit({ 100 | varNameFilter: text 101 | })} /> 102 | 103 | } 104 | const maxWidth = "30vw" 105 | 106 | type RowProps = { 107 | varName: core.ValueText[], 108 | before: core.Value | undefined, 109 | after: core.Value | undefined 110 | } 111 | 112 | const VarTableRow = (props: RowProps) => { 113 | 114 | const { varName, before, after } = props 115 | return 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | {before && } 124 | 125 | 126 | 127 | 128 | {after && } 129 | 130 | 131 | 132 | } 133 | 134 | const ScopeVariables = ({ variables }: { variables: Variables }) => { 135 | return 136 | 137 | 138 | 139 | {variables.map(({ varName, before, after }) => 140 | )} 141 | 142 |
143 | 144 |
145 | } 146 | 147 | export const StepVariables = () => { 148 | const stepDetailData = stepDetail.useData() 149 | 150 | if (stepDetailData === undefined) { 151 | return <> 152 | } 153 | const vars = Object.entries(stepDetailData.variables) 154 | 155 | return <> 156 | 157 | step variables 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | { 167 | vars.map(([scopeName, variables]) => 168 | 169 | {scopeName} 170 | 171 | 172 | ) 173 | } 174 | 175 | 176 | } 177 | 178 | const Provider: React.FC<{}> = ({ children }) => { 179 | return 180 | 181 | 182 | 183 | 184 | 185 | 186 | {children} 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | } 195 | 196 | export const StepDetailPanel = () => { 197 | 198 | return 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | {stepDetail && } 212 | 213 | 214 | 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/panel/VarChangeLogPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as core from "../../../core" 3 | import { Box, TableContainer, Paper, Table, TableHead, TableRow, TableBody, TableCell } from "@mui/material"; 4 | import { HighlightedSpans, HighlightedTree } from "../components/HighlightedTree"; 5 | import { ValueShowSettings } from "../components/ValueShowSettings"; 6 | import { StepLink } from "../components/StepLink"; 7 | import { SearchBar } from "../components/SearchBar"; 8 | import { SelectMultipleList, SelectOneList } from "../components/SelectList"; 9 | import { varChangeLogResult, varChangeLogUserInput, reloadShowOptions, noReloadShowOptions } from "../accessor"; 10 | 11 | import { CircularBackdrop } from "../components/CircularBackdrop"; 12 | 13 | 14 | const VarTableHeader = () => { 15 | return 16 | 17 | step 18 | scope 19 | variable 20 | value 21 | 22 | 23 | } 24 | const maxValueWidth = "calc(70vw - 17em)" 25 | 26 | const SearchValue = () => { 27 | const userInput = varChangeLogUserInput.useData() 28 | 29 | return { 33 | varChangeLogUserInput.sendEdit({ page: 1 }) 34 | varChangeLogResult.callProc("load") 35 | }} 36 | onChange={text => varChangeLogUserInput.sendEdit({ 37 | valueFilter: text 38 | })} /> 39 | } 40 | 41 | const SearchVarName = () => { 42 | const userInput = varChangeLogUserInput.useData() 43 | return { 47 | varChangeLogUserInput.sendEdit({ page: 1 }) 48 | varChangeLogResult.callProc("load") 49 | }} 50 | onChange={text => varChangeLogUserInput.sendEdit({ 51 | varNameFilter: text 52 | })} /> 53 | } 54 | 55 | const VarTableSearchRow = () => { 56 | return 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | } 73 | 74 | const VarTableRow = ({ entry }: { entry: ElemOf }) => { 75 | 76 | return 79 | 80 | 81 | 82 | 83 | 84 | {entry.scopeKind} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | } 99 | 100 | const VarTable = () => { 101 | const varChangeLog = varChangeLogResult.useData() 102 | if (varChangeLog === undefined) { 103 | return <> 104 | } 105 | return 106 | 107 | 108 | 109 | 110 | {varChangeLog.contents.map((entry) => )} 111 | 112 |
113 |
114 | 115 | } 116 | 117 | 118 | 119 | 120 | 121 | const PanelHeader = () => { 122 | const userInputData = varChangeLogUserInput.useData() 123 | const varChangeLog = varChangeLogResult.useData() 124 | 125 | return <>{varChangeLog && 126 | 127 | `${i + 1}`)} 131 | onChange={(page) => { 132 | varChangeLogUserInput.sendEdit({ page: parseInt(page) }) 133 | varChangeLogResult.callProc("load") 134 | }} 135 | /> 136 | 137 | 138 | 139 | 140 | } 141 | } 142 | 143 | const Provider: React.FC<{}> = ({ children }) => { 144 | return 145 | 146 | 147 | 148 | {children} 149 | 150 | 151 | 152 | 153 | } 154 | 155 | const Main = () => { 156 | const varChangeLog = varChangeLogResult.useData() 157 | const [loading, setLoading] = React.useState(true) 158 | React.useEffect(() => { 159 | if (varChangeLog?.loading === false) { 160 | setLoading(false) 161 | } else { 162 | setLoading(true) 163 | } 164 | }, [varChangeLog?.loading]) 165 | 166 | return <> 167 | 168 | 169 | {loading && } 170 | 171 | } 172 | 173 | export const VarChangeLogPanel = () => { 174 | return 175 |
176 |
177 | } 178 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/sidebar/DumpConf.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Button, FormControl, IconButton, InputLabel, Menu, MenuItem, Select, TextField, Typography } from "@mui/material" 3 | import { Add, FileOpen, Remove, Save } from "@mui/icons-material"; 4 | import { dumpConf, logFile, panelOpen } from "../accessor"; 5 | import { DumpConfig } from "../../../dumper/lib"; 6 | 7 | 8 | 9 | const SelectLanguage = () => { 10 | const userInput = dumpConf.useData() 11 | if (userInput === undefined) { 12 | return <> 13 | } 14 | return 15 | language 16 | 24 | 25 | } 26 | 27 | const InputCommand = () => { 28 | const userInput = dumpConf.useData() 29 | const [execCommand, setExecCommand] = React.useState(userInput?.execCommand) 30 | React.useEffect(() => { 31 | setExecCommand(userInput?.execCommand) 32 | }, 33 | [userInput?.execCommand]) 34 | 35 | if (userInput === undefined) { 36 | return <> 37 | } 38 | return <> 39 | 40 | { 46 | // workaround of https://github.com/mui/material-ui/issues/4430 47 | setExecCommand(event.target.value) 48 | dumpConf.sendEdit({ 49 | execCommand: event.target.value 50 | }) 51 | } 52 | } 53 | InputLabelProps={{ shrink: true }} 54 | fullWidth 55 | /> 56 | 57 | 58 | } 59 | 60 | type Options = DumpConfig["options"] 61 | type OptionName = ElemOf["optionName"] 62 | 63 | const AddOptions = () => { 64 | const userInput = dumpConf.useData() 65 | const currentOptions = userInput?.options ?? [] 66 | 67 | const [anchorEl, setAnchorEl] = React.useState(null); 68 | const open = Boolean(anchorEl); 69 | 70 | const optionNameToDisplay = { 71 | "targetDir": "src directory", 72 | "targetModule": "analysis target module", 73 | "stdin": "stdin" 74 | } as const 75 | const optionNames: OptionName[] = 76 | ["targetDir", "targetModule", "stdin"] 77 | 78 | const isMultilineOption = (optionName: OptionName) => { 79 | return optionName === "stdin" 80 | } 81 | 82 | const addRow = (optionName: OptionName) => { 83 | const newOptions = [...currentOptions, { optionName, value: "" }] 84 | dumpConf.sendEdit({ options: newOptions }) 85 | setAnchorEl(null) 86 | } 87 | 88 | const removeRow = (index: number) => { 89 | const newOptions = currentOptions.filter((_x, i) => i !== index) 90 | dumpConf.sendEdit({ options: newOptions }) 91 | } 92 | 93 | const handleChange = (index: number, value: string) => { 94 | const newOptions = currentOptions.map((x, i) => i === index ? { ...x, value } : x) 95 | dumpConf.sendEdit({ options: newOptions }) 96 | } 97 | if (userInput === undefined) { 98 | return <> 99 | } 100 | return 101 | 102 | options 103 | 104 | setAnchorEl(event.currentTarget)}> 108 | 109 | 110 | 111 | 112 | setAnchorEl(null)} 116 | >{ 117 | optionNames.map(optionName => 118 | addRow(optionName)}> 119 | {optionNameToDisplay[optionName]} 120 | 121 | ) 122 | } 123 | 124 | <> 125 | { 126 | currentOptions.map((row, index) => 131 | handleChange(index, e.target.value)} 135 | multiline={isMultilineOption(row.optionName)} 136 | value={row.value} 137 | /> 138 | removeRow(index)}> 139 | 140 | 141 | 142 | ) 143 | } 144 | 145 | 146 | } 147 | 148 | const Actions = () => { 149 | const userInput = dumpConf.useData() 150 | return 151 | 164 | 165 | } 166 | 167 | const Title = () => { 168 | return 169 | config 170 | 171 | dumpConf.callProc("load")}> 175 | 176 | 177 | dumpConf.callProc("save")}> 181 | 182 | 183 | 184 | 185 | } 186 | 187 | const Provider: React.FC<{}> = ({ children }) => { 188 | return 189 | {children} 190 | 191 | } 192 | 193 | export const DumpConfSection = () => { 194 | return 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/sidebar/PanelControlls.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Button, Typography } from "@mui/material" 3 | import { panelOpen } from "../accessor"; 4 | 5 | 6 | export const PanelControlls = () => { 7 | 8 | return <> 9 | panels 10 | 14 | step detail 15 | 22 | 23 | 27 | variable change log 28 | 35 | 36 | 37 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Divider } from "@mui/material" 3 | import { DumpConfSection } from "./DumpConf"; 4 | import { Status } from "./Status"; 5 | import { PanelControlls } from "./PanelControlls"; 6 | 7 | export const Sidebar = () => { 8 | 9 | return <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/sidebar/Status.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, IconButton, Typography } from "@mui/material" 3 | 4 | import { FileOpen, Save } from "@mui/icons-material"; 5 | import { logMetadata, logFile } from "../accessor"; 6 | 7 | export const Status = () => { 8 | const state = logMetadata.subscribe() 9 | if (state === undefined) { 10 | return <> 11 | } 12 | return <> 13 | 14 | status 15 | 16 | logFile.callProc("open")}> 20 | 21 | 22 | logFile.callProc("save")}> 27 | 28 | 29 | 30 | 31 | analysis: {state.status} 32 | max step: {state.maxStep} 33 | 34 | } -------------------------------------------------------------------------------- /src/vscodeExtension/webview/webviewMessaging.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { AnyEditableStateId, StateDataOf } from "../extension/store/state"; 3 | import { ProcNameOf, StateIdOf } from "../extension/store/store"; 4 | import { encodeCallMessage, encodeSubscribeMessage, encodeUpdateMessage, tryDecodeDisplayMessage } from "../messaging"; 5 | 6 | interface VsCode { 7 | postMessage(message: any): void; 8 | getState(): any; 9 | setState(state: any): void; 10 | }; 11 | 12 | declare global { 13 | var vscode: VsCode 14 | } 15 | 16 | export const makeAccessor = < 17 | Store, 18 | Data = StateDataOf>, 19 | EditableData = StateIdOf extends AnyEditableStateId ? StateDataOf> : never, 20 | >( 21 | stateId: StateIdOf 22 | ) => { 23 | const context = React.createContext(undefined) 24 | const subscribe = () => { 25 | const [state, setState] = React.useState() 26 | React.useEffect(() => { 27 | window.addEventListener("message", (event: MessageEvent) => { 28 | const message = event.data 29 | console.log(stateId, "receive") 30 | try { 31 | const data = tryDecodeDisplayMessage(stateId, message) 32 | setState(data as Data) 33 | } catch { 34 | // no nothing when this message is not a target 35 | } 36 | }) 37 | const subscribeMessage = encodeSubscribeMessage(stateId) 38 | vscode.postMessage(subscribeMessage) 39 | }, []) 40 | return state 41 | } 42 | const Provider: React.FC<{}> = ({ children }) => { 43 | const state = subscribe() 44 | return 45 | {children} 46 | 47 | } 48 | const accessor = { 49 | callProc: (procName: ProcNameOf) => { 50 | const message = encodeCallMessage(stateId, procName) 51 | vscode.postMessage(message) 52 | }, 53 | sendEdit: (data: Partial) => { 54 | const message = encodeUpdateMessage(stateId, data) 55 | vscode.postMessage(message) 56 | }, 57 | subscribe, 58 | Provider, 59 | useData: () => { 60 | return React.useContext(context) 61 | } 62 | } 63 | return accessor 64 | } 65 | -------------------------------------------------------------------------------- /src/vscodeExtension/webview/webviewTheme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@mui/material" 2 | import * as React from "react"; 3 | 4 | 5 | // color is #XXXXXX format string 6 | const calcLuminance = (color: string) => { 7 | const red = parseInt(color.substring(1, 3), 16) 8 | const green = parseInt(color.substring(3, 5), 16) 9 | const blue = parseInt(color.substring(5, 7), 16) 10 | // https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color 11 | return 0.2126 * red + 0.7152 * green + 0.0722 * blue 12 | } 13 | 14 | const whiteLuminance = calcLuminance("#FFFFFF") 15 | const blackLuminance = calcLuminance("#000000") 16 | 17 | const isDark = (color: string) => { 18 | const luminance = calcLuminance(color) 19 | const distFromWhite = Math.abs(whiteLuminance - luminance) 20 | const distFromBlack = Math.abs(blackLuminance - luminance) 21 | 22 | return (distFromBlack < distFromWhite) 23 | } 24 | 25 | const useCssValue = (propName: string) => { 26 | const vscodeStyles = getComputedStyle(document.body) 27 | const [cssValue, setCssValue] = React.useState(vscodeStyles.getPropertyValue(propName)) 28 | React.useEffect(() => { 29 | const observer = new MutationObserver(() => 30 | setCssValue(vscodeStyles.getPropertyValue(propName)) 31 | ); 32 | 33 | observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); 34 | }, []) 35 | 36 | return cssValue 37 | } 38 | 39 | 40 | export const useGeneratedTheme = (from: "panel" | "sideBar") => { 41 | const bgColor = useCssValue(`--vscode-${from}-background`) 42 | const fontSize = useCssValue(`--vscode-font-size`) 43 | 44 | const paletteType: "dark" | "light" = isDark(bgColor) ? "dark" : "light" 45 | 46 | const theme = createTheme({ 47 | palette: { 48 | mode: paletteType, 49 | background: { 50 | default: bgColor 51 | } 52 | }, 53 | typography: { 54 | fontSize: parseInt(fontSize, 10) 55 | } 56 | }) 57 | 58 | return createTheme(theme, { 59 | palette: { 60 | primary: { 61 | contrastText: theme.palette.primary[paletteType] 62 | }, 63 | secondary: { 64 | contrastText: theme.palette.secondary[paletteType] 65 | }, 66 | warning: { 67 | contrastText: theme.palette.warning[paletteType] 68 | } 69 | } 70 | }) 71 | 72 | } -------------------------------------------------------------------------------- /test/core.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as childProcess from 'child_process' 3 | import { SqliteLogLoarder } from "../src/logLoader/sqliteLogLoader" 4 | import * as core from "../src/core" 5 | import * as path from "path" 6 | import { makeValueText } from "../src/core/entity/value" 7 | 8 | 9 | const dumperPath = "src/dumper/python/main.py" 10 | const rootPath = "./test/target_files/python/" 11 | 12 | const toPrimitive = (value: string, typ: string): core.Value => { 13 | 14 | return { children: {}, type: typ, expression: [makeValueText(value)], hasHit: false } 15 | } 16 | 17 | describe("var change log perf", () => { 18 | const outPath = path.join(rootPath, "httpie.db") 19 | test("httpie query from cache < 1s", async () => { 20 | const loader = new SqliteLogLoarder(outPath) 21 | const vt = new core.VarTrace(loader) 22 | await vt.getVarChangeLog({ 23 | page: 1, pageSize: 10000, 24 | }) 25 | const start = Date.now() 26 | await vt.getVarChangeLog({ 27 | page: 1, pageSize: 10000 28 | }) 29 | const end = Date.now() 30 | const sec = (end - start) / 1000 31 | console.log(`done in ${sec} sec`) 32 | expect(sec).toBeLessThan(1); 33 | }) 34 | 35 | test("httpie query from cache with search < 1s", async () => { 36 | const loader = new SqliteLogLoarder(outPath) 37 | const vt = new core.VarTrace(loader) 38 | await vt.getVarChangeLog({ 39 | page: 1, pageSize: 10000, 40 | }) 41 | const start = Date.now() 42 | await vt.getVarChangeLog({ 43 | page: 1, pageSize: 10000, 44 | valueFilter: "default" 45 | }) 46 | const end = Date.now() 47 | const sec = (end - start) / 1000 48 | console.log(`done in ${sec} sec`) 49 | expect(sec).toBeLessThan(1); 50 | }) 51 | }) 52 | 53 | describe("step variables of func", () => { 54 | const outPath = path.join(rootPath, "func.py.db") 55 | const loader = new SqliteLogLoarder(outPath) 56 | const vt = new core.VarTrace(loader) 57 | test("step 4 variable a before is not exists", async () => { 58 | const stepVariables = await vt.getStepVars({ step: 4, varNameFilter: "", showIgnored: true }) 59 | const local_a_before = stepVariables['local'] 60 | .find(({ varName }) => varName[0]?.text === "a") 61 | ?.before 62 | expect(local_a_before).toEqual(undefined) 63 | }) 64 | test("step 4 variable a after is 1", async () => { 65 | const stepVariables = await vt.getStepVars({ step: 4, varNameFilter: "", showIgnored: true }) 66 | const local_a_after = stepVariables['local'] 67 | .find(({ varName }) => varName[0]?.text === "a") 68 | ?.after 69 | let num = toPrimitive("1", "number") 70 | num.expression[0]!.isNew = true 71 | expect(local_a_after).toEqual(num) 72 | }) 73 | }) 74 | 75 | describe("step variables of data types", () => { 76 | const outPath = path.join(rootPath, "data_types.py.db") 77 | const loader = new SqliteLogLoarder(outPath) 78 | const vt = new core.VarTrace(loader) 79 | test("step 24 variable self before", async () => { 80 | const stepVariables = await vt.getStepVars({ step: 24, varNameFilter: "", showIgnored: true }) 81 | const local_self_before = stepVariables['local'] 82 | .find(({ varName }) => varName[0]?.text === "self") 83 | ?.before 84 | const expected = { 85 | children: {}, 86 | expression: [ 87 | { hit: [], text: 'TestClass(' }, 88 | { hit: [], text: ')', } 89 | ], 90 | type: 'TestClass', 91 | hasHit: false 92 | } 93 | expect(local_self_before).toEqual(expected) 94 | }) 95 | }) 96 | describe("files", () => { 97 | const outPath = path.join(rootPath, "small_loop.py.db") 98 | const loader = new SqliteLogLoarder(outPath) 99 | const vt = new core.VarTrace(loader) 100 | test("file list first elem is small_loop.py", async () => { 101 | const file = (await vt.getFiles()).map(x => x.absPath) 102 | expect(file[0]).toMatch(/small_loop\.py$/) 103 | }) 104 | } 105 | ) 106 | describe("line variables", () => { 107 | const outPath = path.join(rootPath, "small_loop.py.db") 108 | const loader = new SqliteLogLoarder(outPath) 109 | const vt = new core.VarTrace(loader) 110 | test("line 4 steps", async () => { 111 | const filePath = (await vt.getFiles()).map(x => x.absPath) 112 | const query = { 113 | fileAbsPath: filePath[0]!, 114 | line: 4, 115 | } 116 | const steps = await vt.getLineSteps(query) 117 | expect(steps).toEqual([4, 7, 10, 13, 16]) 118 | }) 119 | }) 120 | 121 | describe('unnormal exit', () => { 122 | test('uncaught exception', async () => { 123 | const outPath = path.join(rootPath, "exception.py.db") 124 | const loader = new SqliteLogLoarder(outPath) 125 | const vt = new core.VarTrace(loader) 126 | const metadata = await vt.getMetadata() 127 | if (metadata === undefined) { 128 | throw new Error('undefined metadata') 129 | } 130 | const maxStep = metadata.maxStep 131 | const lastVars = await vt.getStepVars({ step: maxStep, varNameFilter: "", showIgnored: true }) 132 | const target = lastVars.global.find(({ varName }) => varName[0]?.text === "y") 133 | expect(target?.before?.expression[0]?.text).toEqual('"should be caught"') 134 | 135 | const lastStepInfo = await vt.getStepInfo(maxStep) 136 | expect(lastStepInfo.line).toEqual(12) 137 | }) 138 | test('assert', async () => { 139 | const outPath = path.join(rootPath, "assert.py.db") 140 | const loader = new SqliteLogLoarder(outPath) 141 | const vt = new core.VarTrace(loader) 142 | const metadata = await vt.getMetadata() 143 | if (metadata === undefined) { 144 | throw new Error('undefined metadata') 145 | } 146 | const maxStep = metadata.maxStep 147 | const lastStepInfo = await vt.getStepInfo(maxStep) 148 | expect(lastStepInfo.line).toEqual(4) 149 | }) 150 | test('exit', async () => { 151 | const outPath = path.join(rootPath, "exit.py.db") 152 | const loader = new SqliteLogLoarder(outPath) 153 | const vt = new core.VarTrace(loader) 154 | const metadata = await vt.getMetadata() 155 | if (metadata === undefined) { 156 | throw new Error('undefined metadata') 157 | } 158 | const maxStep = metadata.maxStep 159 | const lastStepInfo = await vt.getStepInfo(maxStep) 160 | expect(lastStepInfo.line).toEqual(3) 161 | }) 162 | }) -------------------------------------------------------------------------------- /test/dump.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as childProcess from 'child_process' 3 | import { SqliteLogLoarder } from "../src/logLoader/sqliteLogLoader" 4 | import * as core from "../src/core" 5 | import * as path from "path" 6 | import { makeValueText } from "../src/core/entity/value" 7 | 8 | 9 | const dumperPath = "src/dumper/python/main.py" 10 | const rootPath = "./test/target_files/python/" 11 | 12 | const toPrimitive = (value: string, typ: string): core.Value => { 13 | 14 | return { children: {}, type: typ, expression: [makeValueText(value)], hasHit: false } 15 | } 16 | 17 | 18 | const dump = (outFile: string, targetPath: string) => { 19 | 20 | const dumpCommand = `python3 ${dumperPath} -o ${outFile} -M __main__ ${targetPath} ` 21 | if (fs.existsSync(outFile)) { 22 | fs.unlinkSync(outFile) 23 | } 24 | try { 25 | childProcess.execSync(dumpCommand) 26 | } 27 | catch (e) { 28 | // ignore error because some test target are fail cases 29 | } 30 | } 31 | 32 | describe("makeTargetDumps", () => { 33 | 34 | test("makeTargetDumps", () => { 35 | const targets = fs.readdirSync(rootPath) 36 | 37 | for (let target of targets) { 38 | if (!target.endsWith(".py")) { 39 | continue 40 | } 41 | 42 | const targetPath = path.join(rootPath, target) 43 | const outFile = targetPath + ".db" 44 | dump(outFile, targetPath) 45 | expect(fs.existsSync(outFile)).toBe(true); 46 | } 47 | }) 48 | 49 | test("make httpie dump", () => { 50 | const start = Date.now() 51 | const outFile = rootPath + "httpie.db" 52 | if (fs.existsSync(outFile)) { 53 | fs.unlinkSync(outFile) 54 | } 55 | const dumpCommand = `python ${dumperPath} -o ${outFile} -M httpie -m httpie google.com` 56 | childProcess.execSync(dumpCommand) 57 | const end = Date.now() 58 | const sec = (end - start) / 1000 59 | console.log(`done in ${sec} sec`) 60 | expect(sec).toBeLessThan(20); 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/dump_conf/data_types.vt_dump_conf: -------------------------------------------------------------------------------- 1 | {"language":"python","execCommand":"python3 c:\\code\\vartrace\\test\\target_files\\python\\data_types.py","options":[{"optionName":"targetModule","value":"httpie"}]} -------------------------------------------------------------------------------- /test/dump_conf/httpie.vt_dump_conf: -------------------------------------------------------------------------------- 1 | {"language":"python","execCommand":"python -m httpie google.com","options":[{"optionName":"targetModule","value":"httpie"}]} -------------------------------------------------------------------------------- /test/target_files/python/assert.py: -------------------------------------------------------------------------------- 1 | 2 | assert True 3 | x = 3 4 | assert False 5 | -------------------------------------------------------------------------------- /test/target_files/python/data_types.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | dic_mod = {"a": 1} 5 | dic_mod["a"] = 2 6 | dic_add = {"a": 1} 7 | dic_add[3] = 2 8 | list_add = [1] 9 | list_add.append(2) 10 | list_mod = [1, 2] 11 | list_mod[1] = 3 12 | list_del = [1, 2] 13 | list_del.pop() 14 | nest = {"a": {"c": "d"}, "b": [1, 2], "c": set([1, 2])} 15 | 16 | rec = [] 17 | rec.append(rec) 18 | 19 | x = range(3) 20 | y = map(str, [1, 2, 3]) 21 | x = tuple(x) 22 | 23 | 24 | class TestClass: 25 | def __init__(self): 26 | self.x = 1 27 | 28 | 29 | test_class = TestClass() 30 | 31 | longlonglonglonglonglongstring = "long"*100 32 | 33 | longlong_dic = { 34 | "longlonglonglonglonglongstring": "long"*100, 35 | "longlonglonglonglonglongstring2": "long"*100 36 | } 37 | 38 | ignored_by_huge = [0] * 1000 39 | 40 | inf = float("inf") 41 | 42 | tbl = [[1, 2, 4], [5, 5, 7]] 43 | 44 | dic_tbl = {"a": {"b": 1, "c": 4}, "d": {"x": 6}} 45 | 46 | table3d = [[[1], [2, 3]], [[4, 5]], [[6]]] 47 | table3d[1][0][1] = 9 48 | table3d[1][0][0] = 7 49 | 50 | queue = deque([1, 2, 3]) 51 | queue.popleft() 52 | -------------------------------------------------------------------------------- /test/target_files/python/demo.py: -------------------------------------------------------------------------------- 1 | 2 | def collatz(a): 3 | if a == 1: 4 | return 5 | elif a % 2 == 0: 6 | collatz(a//2) 7 | else: 8 | collatz((3*a+1)//2) 9 | 10 | 11 | collatz(10001) 12 | -------------------------------------------------------------------------------- /test/target_files/python/exception.py: -------------------------------------------------------------------------------- 1 | 2 | x = 1 3 | try: 4 | x = 3 5 | raise NotImplementedError("should be caught") 6 | except Exception as e: 7 | y = str(e) 8 | 9 | x = 2 10 | 11 | if x == 2: 12 | raise NotImplementedError("should be uncaught") 13 | 14 | y = "never arrive here" 15 | -------------------------------------------------------------------------------- /test/target_files/python/exit.py: -------------------------------------------------------------------------------- 1 | x = 3 2 | 3 | exit(1) 4 | -------------------------------------------------------------------------------- /test/target_files/python/func.py: -------------------------------------------------------------------------------- 1 | def f(x, y): 2 | a = x 3 | return a + x + y 4 | 5 | 6 | f(1, 2) 7 | -------------------------------------------------------------------------------- /test/target_files/python/huge_loop.py: -------------------------------------------------------------------------------- 1 | data = [0]*100 2 | count = 0 3 | for i in range(4000): 4 | count += 1 5 | 6 | print(count) 7 | -------------------------------------------------------------------------------- /test/target_files/python/multibyte_string.py: -------------------------------------------------------------------------------- 1 | """ 2 | コメント pͪoͣnͬpͣoͥnͭpͣa͡inͥ 😔 3 | """ 4 | 5 | s = """ 6 | ab 7 | b c 8 | b c 9 | あ 10 | 𩸽 11 | "" 12 | pͪoͣnͬpͣoͥnͭpͣa͡inͥ 😔 13 | """ 14 | -------------------------------------------------------------------------------- /test/target_files/python/small.py: -------------------------------------------------------------------------------- 1 | a = 1 2 | a = 2 3 | -------------------------------------------------------------------------------- /test/target_files/python/small_loop.py: -------------------------------------------------------------------------------- 1 | count = 0 2 | for i in range(5): 3 | count += 1 4 | count %= 3 5 | 6 | print(count) 7 | -------------------------------------------------------------------------------- /tsconfig.extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "outDir": "dist", 6 | "lib": [ 7 | "esnext", 8 | "dom", 9 | "dom.iterable", 10 | ], 11 | "sourceMap": true, 12 | "strict": true, 13 | "allowSyntheticDefaultImports": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUncheckedIndexedAccess": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "./../node_modules/**", 23 | ".vscode-test", 24 | "**/webview/**", 25 | "**/common/webview/**", 26 | ] 27 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | "lib": [ 14 | "dom", 15 | "dom.iterable", 16 | "esnext" 17 | ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 18 | "jsx": "react", /* Specify what JSX code is generated. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 24 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "resolveJsonModule": true, /* Enable importing .json files */ 38 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | /* Emit */ 44 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 45 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 46 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 47 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 48 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 49 | "outDir": "dist", /* Specify an output folder for all emitted files. */ 50 | // "removeComments": true, /* Disable emitting comments. */ 51 | // "noEmit": true, /* Disable emitting files from a compilation. */ 52 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 53 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 54 | "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 55 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 58 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 59 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 60 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 61 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 62 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 63 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 64 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 65 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 66 | /* Interop Constraints */ 67 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 68 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 69 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 70 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 71 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 72 | /* Type Checking */ 73 | "strict": true, /* Enable all strict type-checking options. */ 74 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 75 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 76 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 77 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 78 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 79 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 80 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 81 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 82 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 83 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 84 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 85 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 86 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 87 | "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 88 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 89 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 90 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 91 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 92 | /* Completeness */ 93 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 94 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 95 | } 96 | } -------------------------------------------------------------------------------- /tsconfig.webview.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "outDir": "dist", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "sourceMap": true, 12 | "strict": true, 13 | "jsx": "react", 14 | "allowSyntheticDefaultImports": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "./../node_modules/**", 20 | ".vscode-test", 21 | "**/extension/**" 22 | ] 23 | } -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const extensionConfig = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 11 | 12 | entry: './src/vscodeExtension/extension/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | output: { 14 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'index.js', 17 | libraryTarget: 'commonjs' 18 | }, 19 | devtool: 'nosources-source-map', 20 | externals: { 21 | "mysql2": "mysql2", //unused kysely dialect but cause error 22 | "pg": "pg",//unused kysely dialect but cause error 23 | "better-sqlite3": "commonjs better-sqlite3", 24 | sqlite3: 'commonjs sqlite3', 25 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 26 | // modules added here also need to be added in the .vsceignore file 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: ['.ts', '.js'], 31 | }, 32 | 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.ts$/, 37 | use: [ 38 | { 39 | loader: 'ts-loader', 40 | options: { 41 | configFile: "tsconfig.extension.json" 42 | } 43 | } 44 | ] 45 | }, 46 | ] 47 | } 48 | }; 49 | 50 | const webviewConfig = { 51 | entry: "./src/vscodeExtension/webview/index.tsx", 52 | output: { 53 | filename: "webview.js" 54 | }, 55 | devtool: "eval-source-map", 56 | resolve: { 57 | extensions: [".js", ".ts", ".tsx", ".json"] 58 | }, 59 | module: { 60 | rules: [ 61 | { 62 | test: /\.(ts|tsx)$/, 63 | loader: "ts-loader", 64 | options: { 65 | configFile: "tsconfig.webview.json" 66 | } 67 | }, 68 | { 69 | test: /\.css$/, 70 | use: [ 71 | { 72 | loader: "style-loader" 73 | }, 74 | { 75 | loader: "css-loader" 76 | } 77 | ] 78 | } 79 | ] 80 | }, 81 | }; 82 | 83 | 84 | module.exports = [extensionConfig, webviewConfig] --------------------------------------------------------------------------------