├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .huskyrc.js ├── .mocharc.js ├── .npmrc ├── .vscode ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── demo ├── .vscode │ ├── launch.json │ └── settings.json ├── demo-attach.ahk ├── demo-attach.ahk2 ├── demo-stacktrace.ahk ├── demo-stacktrace.ahk2 ├── demo.ahk ├── demo.ahk2 └── lib │ └── Util.ahk ├── gulpfile.ts ├── icon.png ├── image ├── breakpoint-directive.gif ├── call-stack.gif ├── conditional-breakpoint.gif ├── data-inspection-when-hover.gif ├── data-inspection.gif ├── debug-console.jpg ├── hit-conditional-breakpoint.gif ├── intellisense.gif ├── loaded-scripts.gif ├── log-point.gif ├── output-directive.gif ├── perftips.gif ├── rewriting-variables.gif └── watch-expression.gif ├── package-lock.json ├── package.json ├── src ├── CompletionItemProvider.ts ├── ahkDebug.ts ├── commands.ts ├── dbgpSession.ts ├── extension.ts └── util │ ├── AutoHotkeyLuncher.ts │ ├── BreakpointManager.ts │ ├── CaseInsensitiveMap.ts │ ├── ConditionEvaluator.ts │ ├── ConditionParser.ts │ ├── TraceLogger.ts │ ├── VariableManager.ts │ ├── getAhkVersion.ts │ ├── getRunningAhkScriptList.ts │ ├── numberUtils.ts │ ├── stringUtils.ts │ └── util.ts ├── test ├── dbgpSession.test.ts └── index.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { preset } = require('@zero-plusplus/eslint-my-rules'); 2 | 3 | module.exports = { 4 | root: true, 5 | overrides: [ 6 | { 7 | ...preset.js.default, 8 | rules: { 9 | ...preset.js.default.rules, 10 | }, 11 | }, 12 | { 13 | ...preset.ts.default, 14 | rules: { 15 | ...preset.ts.default.rules, 16 | 'no-undefined': 'off', 17 | '@typescript-eslint/explicit-module-boundary-types': 'off', 18 | }, 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: zero-plusplus 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | 4 | .eslintcache 5 | *.vsix 6 | yarn-error.log -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'hooks': { 3 | 'pre-commit': 'npm run lint', 4 | }, 5 | }; -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("mocha").MochaOptions} */ 2 | module.exports = { 3 | package: './package.json', 4 | require: [ 5 | 'source-map-support/register', 6 | 'esbuild-register', 7 | ], 8 | // parallel: true, 9 | bail: true, 10 | ui: 'tdd', 11 | timeout: 999999, 12 | colors: true, 13 | }; -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //npm.pkg.github.com:_authToken=${GITHUB_PACKAGE_TOKEN} 2 | @zero-plusplus:registry=https://npm.pkg.github.com -------------------------------------------------------------------------------- /.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": "Extension Test", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}", 14 | "${workspaceFolder}/demo" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/build/**/*.js" 18 | ], 19 | "preLaunchTask": "subtask:watch", 20 | "skipFiles": [ 21 | "/**", 22 | "**", 23 | "!${workspaceFolder}/**" 24 | ], 25 | }, 26 | { 27 | "name": "Extension Test without lint", 28 | "type": "extensionHost", 29 | "request": "launch", 30 | "runtimeExecutable": "${execPath}", 31 | "args": [ 32 | "--extensionDevelopmentPath=${workspaceFolder}", 33 | "${workspaceFolder}/demo" 34 | ], 35 | "outFiles": [ 36 | "${workspaceFolder}/build/**/*.js" 37 | ], 38 | "preLaunchTask": "subtask:build:without-lint", 39 | }, 40 | { 41 | "name": "Gulp Test:build", 42 | "type": "node", 43 | "request": "launch", 44 | "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", 45 | "args": [ 46 | "build" 47 | ], 48 | "skipFiles": [ 49 | "/**" 50 | ] 51 | }, 52 | { 53 | "name": "Gulp Test:bundle", 54 | "type": "node", 55 | "request": "launch", 56 | "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", 57 | "args": [ 58 | "bundle" 59 | ], 60 | "skipFiles": [ 61 | "/**" 62 | ] 63 | }, 64 | { 65 | "name": "Mocha Tests", 66 | "type": "node", 67 | "request": "launch", 68 | "preLaunchTask": "subtask:build", 69 | "cwd": "${workspaceFolder}", 70 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 71 | "args": [ 72 | "${workspaceFolder}/build/test" 73 | ], 74 | "outFiles": [ 75 | "${workspaceFolder}/build/test/**/*.js" 76 | ], 77 | "internalConsoleOptions": "openOnSessionStart", 78 | "skipFiles": [ 79 | "/**" 80 | ] 81 | }, 82 | { 83 | "name": "Mocha Selected Tests", 84 | "type": "pwa-node", 85 | "request": "launch", 86 | "cwd": "${workspaceFolder}", 87 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 88 | "args": [ 89 | "${file}" 90 | ], 91 | "sourceMaps": true, 92 | "internalConsoleOptions": "openOnSessionStart", 93 | "resolveSourceMapLocations": [ 94 | "${workspaceFolder}/**", 95 | "!**/node_modules/**" 96 | ], 97 | "skipFiles": [ 98 | "/**" 99 | ] 100 | }, 101 | ] 102 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "install-test-vsix", 8 | "type": "shell", 9 | "dependsOn": [ 10 | "subtask:build-test-vsix", 11 | "subtask:install-test-vsix", 12 | "subtask:reload-vscode", 13 | ], 14 | "dependsOrder": "sequence" 15 | }, 16 | { 17 | "label": "subtask:build-test-vsix", 18 | "type": "shell", 19 | "command": "vsce", 20 | "args": [ 21 | "package", 22 | "-o", "extension-test.vsix" 23 | ] 24 | }, 25 | { 26 | "label": "subtask:install-test-vsix", 27 | "type": "shell", 28 | "command": "code", 29 | "args": [ 30 | "--install-extension", "${workspaceFolder}/extension-test.vsix" 31 | ], 32 | "dependsOn": [ "build-test-vsix" ], 33 | }, 34 | { 35 | "label": "subtask:reload-vscode", 36 | "command": "${command:workbench.action.reloadWindow}" 37 | }, 38 | { 39 | "label": "subtask:build", 40 | "type": "shell", 41 | "command": "npm run build" 42 | }, 43 | { 44 | "label": "subtask:build:without-lint", 45 | "type": "shell", 46 | "command": "npm run build:without-lint" 47 | }, 48 | { 49 | "label": "subtask:watch", 50 | "type": "shell", 51 | "isBackground": true, 52 | "command": "npm run watch", 53 | "problemMatcher": [ 54 | { 55 | "owner": "gulp", 56 | "pattern": { 57 | "regexp": "^\\s*([^:]+):(\\d+):(\\d+) - (error) (.+)$", 58 | "file": 1, 59 | "line": 2, 60 | "column": 3, 61 | "severity": 4, 62 | "message": 5, 63 | }, 64 | "background": { 65 | "activeOnStart": true, 66 | "beginsPattern": "^\\s*\\[\\d{1,2}:\\d{1,2}:\\d{1,2}\\] Starting 'watch.*$", 67 | "endsPattern": "(^\\s*\\[\\d{1,2}:\\d{1,2}:\\d{1,2}\\] Finished 'watch' after .+$)|(^\\[esbuild\\] build completed$)" 68 | } 69 | } 70 | ] 71 | }, 72 | ], 73 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .vscode/ 3 | build/**/*.ts 4 | demo/ 5 | image/ 6 | node_modules/ 7 | !node_modules/ts-predicates 8 | !node_modules/typeof-util 9 | !node_modules/vscode-uri 10 | src/ 11 | test/ 12 | .eslint* 13 | .gitignore 14 | .huskyrc.js 15 | .mocharc.js 16 | gulpfile.ts 17 | tsconfig.json 18 | yarn-error.log -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **NOTE: This document has been translated by a translator.** 2 | 3 | # Changelog 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog][Keep a Changelog] and this project adheres to [Semantic Versioning][Semantic Versioning]. 7 | 8 | ## [Unreleased] 9 | If you want to see what the next version of the plan is, check out the [here](https://github.com/zero-plusplus/vscode-autohotkey-debug/labels/milestone). 10 | Also want to check the development status, check the [commit history](https://github.com/zero-plusplus/vscode-autohotkey-debug/commits/develop) of the develop branch. 11 | 12 | --- 13 | 14 | ## [Released] 15 | ## [1.11.1] - 2024-09-23 16 | Support working with [AutoHotkey Dev Tools](https://github.com/zero-plusplus/autohotkey-devtools) to be released in the future 17 | 18 | ## [1.11.0] - 2022-02-11 19 | ### Added 20 | * [#201](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/201) Add `useLoadedScripts` to launch.json 21 | 22 | ### Fixed 23 | * [#189](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/189) Refix. Stopping the debugger adapter due to ECONNRESET error 24 | * [#198](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/198) The error message when ECONNRESET occurs is incorrect 25 | * [#199](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/199) Loaded Scripts process may become extremely slow. broken by `1.10.0` 26 | * [#199](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/199) Debugging will not start if scripts are included that are loaded into each other using `#Include` 27 | * [#199](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/199) [The standard library](https://www.autohotkey.com/docs/Functions.htm#lib) may not be displayed in Loaded scripts, and the debugger directive may not work 28 | * [#202](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/202) Error message is not displayed when a non-existent runtime is specified 29 | * [#203](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/203) If the number of loaded scripts exceeds 60, only the first debug will fail 30 | * [#204](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/204) [AutoHotkey v2-128](https://www.autohotkey.com/boards/viewtopic.php?f=37&t=2120&sid=e7d43fe09e912b95ab2d1747a47f8bad&start=80#p385995) and later versions may show auto-include-libraries in Loaded Scripts that are not actually loaded 31 | * [#205](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/205) When a library call is included in a string or in a comment, files that are not actually loaded may appear in the Loaded Scripts 32 | 33 | ## [1.10.2] - 2022-01-27 34 | ### Fixed 35 | * [#192](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/192) Debug adapter may crash when evaluating variables in hover, watch expressions, etc. 36 | 37 | ## [1.10.1] - 2022-01-26 [YANKED] 38 | **Note: This version is broken. This will be fixed in `1.10.1`.** 39 | 40 | ### Fixed 41 | * [#179](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/179) `Run Without Debugging` does not print any output to the debug console 42 | * [#180](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/180) Hit conditional breakpoint do not work properly if they contain spaces before or after the condition 43 | * [#188](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/188) ComObject child elements may not be displayed correctly in data inspection, watch expression, etc. 44 | * [#188](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/188) 4 errors may occur when displaying ComObject child elements in data inspection 45 | * [#189](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/189) Stopping the debugger adapter due to ECONNRESET error 46 | 47 | ## [1.10.0] - 2021-12-25 48 | ### Added 49 | * [#75](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/75) Add some context menus to copy the value of a variable in data inspection 50 | * [#88](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/88) Add `variableCategories` attribute to launch.json 51 | * [#142](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/142) Add `useAnnounce` attribute to launch.json 52 | * [#142](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/142) Add `useOutputDebug` attribute to launch.json 53 | * [#147](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/147) Add ClearConsole directive to debug directive 54 | * [#148](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/148) Add `Add to Watch` and `Copy as Expression` to context menu of data inspection 55 | * [#150](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/150) Add `{now}`, `{callstack}`, `{callstackNames}`, `{thisCallstack}` to meta variables 56 | * [#151](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/151) Add `{variablesCategories}` to meta variables 57 | * [#154](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/154) Add some context menus to view the value of a variable to data inspection 58 | * [#161](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/161) Add `has` operator to conditional breakpoint 59 | * [#162](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/162) Add `contanis` operator to conditional breakpoint 60 | * [#173](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/173) Add `cwd` attribute to launch.json 61 | * Add a some snippet to launch.json 62 | 63 | ### Changed 64 | * [#157](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/157) The specification has been changed so that when multiple objects are output at log points, etc., they are grouped together and only one message is output 65 | * [#163](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/163) Restored true base objects to `` notation 66 | * [#165](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/165) The hover information has been improved so that it can display the value of properties that contain bracket notation such as `var[key].property` 67 | * [#176](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/176) Add a new format to log points, etc., to specify the depth of the child elements to retrieve, such as `{variableName:depth}` 68 | * Change so that [numerical variables](https://www.autohotkey.com/docs/Variables.htm#CommandLine) representing command line arguments are not suggested by IntelliSense 69 | 70 | ### Fixed 71 | * [#149](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/149) Watch expression only show the contents of the latest call stack 72 | * [#164](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/164) Fix inefficient debugging command call process 73 | * [#167](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/167) `0x0` is not treated as falsy in conditional breakpoint 74 | * [#168](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/168) When a string is specified as bracket notation in a conditional breakpoint, it is not evaluated correctly 75 | * [#169](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/169) When a `true` is specifed in a conditional breakpoint, if it contains capital letters, it will not be evaluated correctly 76 | * [#170](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/170) If the script exits after using a conditional breakpoint that returns false, an error ECONNRESET may occur 77 | * [#171](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/171) Cannot correctly retrieve children of object with key containing linebreak 78 | * [#177](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/177) Whenever a conditional breakpoint contains a space in front of the condition, it is evaluated as `false` 79 | * [#178](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/178) v2 only bug. Bracket notation suggestion displays properties that are not actually available 80 | * When `"attach"` is specified for request when editing launch.json, `skipFiles` and `skipFunctions` are not displayed in the IntelliSense 81 | * The breakpoint operation may fail 82 | 83 | ## [1.9.0] - 2021-10-03 84 | ### Added 85 | * [#69](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/69) Support `skipFiles` and `skipFunctions` attribute in launch.json 86 | * [#143](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/143) Support `extends` attribute in launch.json 87 | 88 | ## [1.8.0] - 2021-09-23 89 | ### Added 90 | * [#67](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/67) Support attach mode 91 | * [#78](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/78) `useUIAVersion` to launch.json 92 | 93 | ### Changed 94 | * [#129](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/129) Only when using `useAutoJumpToError`. When jumping to an error, highlight the jump destination for a short while 95 | * [#131](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/131) Enable the disconnect button 96 | 97 | ### Fixed 98 | * [#130](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/130) When the error code is `0`, the debug exit message is not displayed 99 | * [#133](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/133) v2 only bug. Debugging crashes when trying to look at a child element of an instance of a class with `__Enum` meta-function 100 | * [#135](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/135) v2 only bug. Hovering over `&variable` does not show variable information 101 | * [#135](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/135) Information is not displayed when uninitialized variable names are hovered over 102 | * [#137](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/137) If `${file}` is set to `openFileOnExit` when the editor is not open, an error occurs and debugging cannot be started 103 | * [#138](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/138) Conditional breakpoints do not recognize boolean values 104 | * [#139](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/139) v2 only bug. Conditional breakpoints cannot compare numbers in scientific notation correctly 105 | 106 | ## [1.7.1] - 2021-08-17 107 | ### Fixed 108 | * [#118](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/118) `Copy Value` does not work in Variables View 109 | 110 | ## [1.7.0] - 2021-08-17 111 | ### Added 112 | * [#54](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/54) `useAutoJumpToError` to launch.json 113 | 114 | ## [1.6.14] - 2021-07-27 115 | ### Fixed 116 | * [#112](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/112) AutoHotkey2-beta.1 does not set `/ErrorStdOut=UTF-8` automatically 117 | 118 | ## [1.6.13] - 2021-07-16 119 | ### Fixed 120 | * [#111](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/111) Inherited child elements are not displayed on hover 121 | 122 | ## [1.6.12] - 2021-07-09 123 | ### Fixed 124 | * [#110](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/110) Suppressing an error when retrieving a non-existent key in bracket notation 125 | 126 | ## [1.6.11] - 2021-07-08 127 | ### Fixed 128 | * [#108](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/108) v2 only bug. In data inspection, dynamic property values may not be displayed correctly 129 | * [#109](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/109) v2 only bug. In data inspection, etc., significant performance degradation when retrieving child elements of objects with depths greater than 7. e.g. `a.b.c.d.e.f.g` 130 | 131 | ## [1.6.10] - 2021-07-04 132 | ### Changed 133 | * Intellisense now works with dot notation (e.g. `object["key"]`) 134 | * Variable name (e.g. `map[key]`, `map[obj.field]`) can now be specified for bracket notation access in Conditional breakpoint, Log point, and Watch expression 135 | 136 | ### Fixed 137 | * [#104](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/104) Inherited members are not displayed in IntelliSense 138 | 139 | ## [1.6.9] - 2021-03-15 140 | ### Changed 141 | * Change the font used for the main icon 142 | 143 | ### Fixed 144 | * [#95](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/95) Slow performance of conditional breakpoints, and crashes after 6000 more than uses 145 | 146 | ## [1.6.8] - 2021-03-02 147 | ### Fixed 148 | * [#91](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/91) v2 only bug. The value of the variable with the specific data becomes `` 149 | 150 | ## [1.6.7] - 2021-02-16 151 | ### Fixed 152 | * [#89](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/89) Elements of sparse array is collapsed unnecessarily 153 | 154 | ## [1.6.6] - 2021-01-17 155 | ### Fixed 156 | * [#84](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/84) If a script changed state waiting due by step execution, PerfTips will remain visible 157 | * [#85](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/85) Pause button is not working when the script is in a waiting state 158 | * [#86](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/86) v2 only bug. When watch expression contains more than one expression, the value may not be retrieved correctly 159 | * [#87](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/87) Some error messages may displayed in the debug console will be part missing 160 | 161 | ## [1.6.5] - 2020-12-02 162 | ### Changed 163 | * Bundled the extension files and also removed unnecessary files to run. This reduced the file size by a tenth and greatly improved the installation speed 164 | 165 | ### Fixed 166 | * [#73](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/73) Sparse array being treated as array in `Data inspection` 167 | * v2 only bug. `Loaded scripts` will not work properly if a relative path is used for `#Include` 168 | 169 | ## [1.6.4] - 2020-11-02 170 | ### Fixed 171 | * [#68](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/68) Debug adapter does not exit successfully if a syntax error occurs 172 | 173 | ## [1.6.3] - 2020-11-02 [YANKED] 174 | **Don't install. Please use `1.6.4`** 175 | 176 | ### Fixed 177 | * [#65](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/65) If a conditional breakpoint that returns false is passed, the pause button will not work until the next stop 178 | * [#66](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/66) When resume debugging, PerfTips will remain visible until the next stop 179 | * Improve termination process 180 | 181 | ## [1.6.2] - 2020-10-26 182 | ### Changed 183 | * [#56](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/56) Whenever possible, the default value of `runtimeArgs` is set to `[ "ErrorStdOut=UTF-8"]` 184 | * [#64](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/64) Default value of `port` to `9002` 185 | * Improve error message when an invalid value is set by launch.json 186 | 187 | ### Fixed 188 | * [#63](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/63) Can't get environment variables in the script 189 | 190 | ## [1.6.1] - 2020-10-06 191 | ### Fixed 192 | * [#21](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/21) Special characters are not escaped in data inspection, etc 193 | 194 | ## [1.6.0] - 2020-10-04 195 | ### Added 196 | * [#13](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/13) Support `Run Without Debugging` 197 | * [#28](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/28) Support MetaVariable. This is supported by several features. 198 | * [#29](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/29) Support PerfTips 199 | * [#30](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/30) Support Debug directive 200 | * [#40](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/40) Support IntelliSense, which is only available for debugging 201 | 202 | ### Changed 203 | * [#27](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/27) Remove Advanced output 204 | * [#35](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/35) The exit code is now always displayed 205 | * [#41](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/41) Remove useAdvancedBreakpoint. Advanced breakpoint is enabled by default 206 | * [#46](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/46) Improved step execution when using Advanced breakpoint. This removed the forced stop 207 | 208 | ### Fixed 209 | * [#32](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/32) If you set a blank character to a log point, it will not be paused until re-set it 210 | * [#33](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/33) Float values do not work properly at conditional breakpoint 211 | * [#34](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/34) The pause and force stop don't work after an advanced breakpoint 212 | * [#37](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/37) Hit Conditonal Breakpoint's `%` operator is not working 213 | * [#44](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/44) Loaded scripts are not detected when on previous #Include line a directory is specified 214 | * [#45](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/45) Loaded scripts are not detected when on specified relative path by #Include 215 | * [#48](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/48) If more than one breakpoints in the same line is removed at once, it will not be removed correctly 216 | * [#49](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/49) v1 only bug. `undefinedVariable == ""` returns false 217 | * [#50](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/50) The base field cannot be inspected by a hover 218 | * [#51](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/51) Error occurs when getting dynamic properties by data inspect, etc 219 | * [#53](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/53) Setting a string containing `&` and `|` in a conditional breakpoint always returns false 220 | * [#55](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/55) Chunking doesn't work when a large array is specified in a Watch expression 221 | * [#57](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/57) If the script exits without stopping at a breakpoint, etc., the message containing the object is not printed correctly 222 | * [#58](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/58) Registering or unregistering a breakpoint resets the hit count for all breakpoints 223 | * [#59](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/59) Hit count shifts when pausing while using Advanced breakpoint 224 | * Data inspect shows an array of length 1 as `{1: value}` 225 | * If the error code is 0, output category is stderr 226 | * Blank line printed when outputting object 227 | * In some case, "\{" is output without unescaped in Log point etc 228 | * In some case, debugging may not be successful 229 | * When the Advanced breakpoint is used, the step execution may cause the {hitCount} to go wrong 230 | 231 | ## [1.5.0] - 2020-08-14 232 | ### Added 233 | * Operators in conditional breakpoint 234 | * The following operators are now available 235 | * `!~` 236 | * `is` 237 | * `in` 238 | * `&&` 239 | * `||` 240 | * `countof` 241 | 242 | ### Changed 243 | * Conditional breakpoint 244 | * JavaScript RegExp is now available with the `~=` operator 245 | * Make `VariableName` parsing more accurate 246 | 247 | ### Fixed 248 | * The exit process fails with some errors 249 | * In some cases, the `` field of an instance cannot be read-write correctly 250 | * Fail to parse hexadecimal numbers starting from 0 as in `0x012` with conditional breakpoints and variable writing, etc 251 | * v1 only bug. Where some variables cannot be obtained with conditional breakpoint and watch expression, etc 252 | 253 | ## [1.4.10] - 2020-08-03 254 | ### Changed 255 | * The object summary to show only the elements that are actually enumerated (i.e. the base property is not shown) 256 | 257 | ### Fixed 258 | * A bug in data inspect 259 | * The overview of objects within an object is not displayed correctly. Occurred in 1.4.8 260 | * Chunking does not work when opening an array of 101 or more in an object. Occurred in 1.4.8 261 | 262 | ## [1.4.9] - 2020-07-27 263 | ### Fixed 264 | * Some runtime error output does not include a newline at the end of the output. Occurred in 1.4.8 265 | 266 | ## [1.4.8] - 2020-07-22 [YANKED] 267 | ### Changed 268 | * Add links to files in some runtime error messages 269 | 270 | ### Fixed 271 | * Debugging does not end normally when some errors occur 272 | * Optimization of data inspect. Previously, the same data was retrieved multiple times 273 | * [#24](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/24) v2 only bug. An error occurs when checking a [property](https://lexikos.github.io/v2/docs/Objects.htm#Custom_Classes_property) with data inspect 274 | 275 | ### Security 276 | * [#23](https://github.com/zero-plusplus/vscode-autohotkey-debug/pull/23) Update vulnerable packages(lodash) 277 | 278 | ## [1.4.7] - 2020-07-16 279 | ### Changed 280 | ### Fixed 281 | * launch.json warns that `program` is not specified 282 | * Document 283 | * Corrected the description of `VariableName` 284 | 285 | ## [1.4.6] - 2020-07-13 286 | ### Changed 287 | * Process the file information output by [#Warn](https://www.autohotkey.com/docs/commands/_Warn.htm) so that vscode can recognize it as a link 288 | 289 | ### Fixed 290 | * In Loaded Scripts 291 | * The commented [#Include](https://www.autohotkey.com/docs/commands/_Include.htm) directive is loaded. If that fails to load, the debug UI goes wrong 292 | * The script itself is not displayed 293 | 294 | ## [1.4.5] - 2020-07-03 295 | ### Fixed 296 | * Conditional breakpoint only bug. Some escape sequences do not work 297 | 298 | ## [1.4.4] - 2020-07-02 299 | ### Fixed 300 | * Conditional breakpoint only bug 301 | * [#19](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/19) v2 only bug. Single quotation string doesn't work 302 | * Escape sequences do not work 303 | 304 | ## [1.4.3] - 2020-06-27 305 | ### Changed 306 | * Rewritten the document using the DeepL translate 307 | 308 | ### Fixed 309 | * The output in the logpoint does not contain line feeds. 310 | * The output ends in `"\0"` 311 | 312 | ## [1.4.2] - 2020-06-26 313 | ### Changed 314 | * [#17](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/17) The `port` of launch.json has been extended to include 315 | It is now possible to declare a range of ports to be used. This allows you to suppress confirmation messages 316 | 317 | ### Fixed 318 | [#16](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/16) The link to `Advanced breakpoint` is broken in README (`Details` from vscode) 319 | 320 | ## [1.4.1] - 2020-06-23 321 | ### Fixed 322 | * v1 only bug; you can rewrite a variable with Integer scientific notation. It is not allowed in v1 323 | * v1 only. Cannot use `` in watch expression 324 | * The icon will remain in the system tray when the debug is finished 325 | * [#15](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/15) v2-a112 only bug. Rewrote the error message to treat the path as a link containing a line number, but `a112` Then it won't work 326 | 327 | ## [1.4.0] - 2020-06-16 328 | ### Added 329 | * `runtimeArgs` and `runtimeArgs_v1` and `runtimeArgs_v2` to launch.json 330 | 331 | ### Changed 332 | * Output the AutoHotkey launch command 333 | 334 | ### Fixed 335 | * [#14](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/14) The link to `issues` is broken in README (`Details` from vscode) 336 | 337 | ## [1.3.7] - 2020-06-13 338 | ### Important Notices 339 | This is a notification for people who debug using launch.json. 340 | As of version 1.3.7, the `type` of launch.json has been changed from `ahk` to `autohotkey`. 341 | This is to prevent it from being misinterpreted as a file extension. 342 | You need to edit the launch.json as soon as possible. 343 | 344 | ### Changed 345 | * Debugger type from `ahk` to `autohotkey` 346 | 347 | ## [1.3.6] - 2020-06-13 348 | ### Fixed 349 | * If a script that contains parentheses in the file name produces a loadtime error, the path is displayed a little oddly in be 350 | 351 | ## [1.3.5] - 2020-06-12 352 | ### Changed 353 | * If you want to debug multiple source codes at the same time, a dialog box appears to confirm whether you want to use another port for debugging 354 | 355 | ## [1.3.4] - 2020-06-10 356 | ### Fixed 357 | * A bug when 'useAdvancedBreakpoint' is true. When a conditional breakpoint or a line with a logpoint is passed by step-in, out, or over, the execution continues until the next breakpoint. Changed it to stop regardless of the condition 358 | 359 | ## [1.3.3] - 2020-06-09 360 | ### Changed 361 | * Support for lazy loading of stacked frames. When the stack frame exceeds 20, loading is lazy 362 | * [#10](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/10) Change the message to allow vscode to recognize the file path, including the line number, as a link if an AutoHotkey error occurs 363 | 364 | ### Fixed 365 | * [#7](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/7) Pressing pause while idling does not display any variables. Not a complete fix due to a specification issue. You must click on the stack frame to view it 366 | 367 | ## [1.3.2] - 2020-06-09 368 | ### Changed 369 | * You can jump to the output source by clicking on the file name to the right of the output, but only if you are outputting at a log point 370 | * Supporting object values in conditional expressions 371 | 372 | ### Fixed 373 | * When outputting a variable to the debug console, it may refer to a variable of a different scope 374 | * Incorrectly displaying an object's overview 375 | * We can't set the `obj.` in the Watch expression, this is limited to v2. This is restricted to v2. (2020/06/27 This was a mistake and was retracted in 1.4.1) 376 | * Can't rewrite the fields of an object 377 | * If write an Integer, it will be treated as a Float 378 | * The conditional breakpoint was not working 379 | 380 | ### Removed 381 | * Message at the end of the debugging process Shows how long it took to debug. I didn't need it 382 | 383 | ## [1.3.1] - 2020-06-07 384 | ### Fixed 385 | * [#3](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/3) The breakpoint fails if the file name contains an embedded space followed by a hyphen 386 | 387 | ## [1.3.0] - 2020-06-07 388 | ### Added 389 | * `openFileOnExit` to launch.json 390 | * `useAdvancedOutput` to launch.json 391 | * Global variable `A_DebuggerName`. Followed Scite4Autohotkey 392 | 393 | ### Changed 394 | * Enhanced standard output 395 | * Supports object output with `Log point` 396 | * Supports functions or commands that print to standard output 397 | * [FileAppend](https://www.autohotkey.com/docs/commands/FileAppend.htm) 398 | * [FileOpen](https://www.autohotkey.com/docs/commands/FileOpen.htm) 399 | * [OutputDebug](https://www.autohotkey.com/docs/commands/OutputDebug.htm) 400 | * Output runtime error to standard error 401 | 402 | ### Fixed 403 | * The pause and restart didn't work 404 | * Can't get the child elements of an object in a `Watch expression` 405 | * Item access with strings like `obj["spaced key"]` does not work with `Watch expression` 406 | 407 | ## [1.2.0] - 2020-05-30 408 | ### Added 409 | * The setting item of `env` to launch.json 410 | 411 | ### Changed 412 | * Warn when the value assigned to `args` of `launch.json` is not a string 413 | 414 | ## [1.1.0] - 2020-05-27 415 | ### Added 416 | * The setting item of `args` to launch.json 417 | 418 | ## [1.0.5] - 2020-05-27 [YANKED] 419 | ### Fixed 420 | * If no launch.json is created or `program` is omitted, the Debugging fails. This bug was introduced in 1.0.4 421 | 422 | ## [1.0.4] - 2020-05-27 423 | **Note: This version is broken. This will be fixed in 1.0.5.** 424 | 425 | ### Fixed 426 | * Debugging fails if you use [variables](https://code.visualstudio.com/docs/editor/variables-reference) in the `runtime` in launch.json. e.g. `"$ {workspaceFolder}/AutoHotkey.exe"` 427 | 428 | ## [1.0.3] - 2020-05-26 [YANKED] 429 | ### Fixed 430 | * Setting a breakpoint while waiting for a script does not work 431 | * Comparing an empty character with a conditional breakpoint returns false 432 | 433 | ## [1.0.2] - 2020-05-26 [YANKED] 434 | ### Fixed 435 | * If the script is in [persistent mode](https://www.autohotkey.com/docs/commands/_Persistent.htm), the process stops when you leave the scope 436 | * Debugging will fail if the script path is a UNC path 437 | 438 | ## [1.0.1] - 2020-05-25 439 | ### Changed 440 | * If the runtime does not exist, an error will be displayed and debugging will stop. Previously, the user had to stop debugging 441 | 442 | ## [1.0.0] - 2020-05-23 443 | First released 444 | 445 | --- 446 | 447 | 448 | [Keep a Changelog]: https://keepachangelog.com/ 449 | [Semantic Versioning]: https://semver.org/ 450 | 451 | 452 | [1.11.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.11.0..v1.11.1 453 | [1.11.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.10.2..v1.11.0 454 | [1.10.2]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.10.1..v1.10.2 455 | [1.10.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.10.0..v1.10.1 456 | [1.10.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.9.0..v1.10.0 457 | [1.9.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.8.0..v1.9.0 458 | [1.8.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.7.1..v1.8.0 459 | [1.7.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.7.0..v1.7.1 460 | [1.7.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.14..v1.7.0 461 | [1.6.14]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.13..v1.6.14 462 | [1.6.13]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.12..v1.6.13 463 | [1.6.12]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.11..v1.6.12 464 | [1.6.11]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.10..v1.6.11 465 | [1.6.10]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.9..v1.6.10 466 | [1.6.9]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.8..v1.6.9 467 | [1.6.8]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.7..v1.6.8 468 | [1.6.7]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.6..v1.6.7 469 | [1.6.6]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.5..v1.6.6 470 | [1.6.5]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.4..v1.6.5 471 | [1.6.4]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.3..v1.6.4 472 | [1.6.3]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.2..v1.6.3 473 | [1.6.2]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.1..v1.6.2 474 | [1.6.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.6.0..v1.6.1 475 | [1.6.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.5.0..v1.6.0 476 | [1.5.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.10..v1.5.0 477 | [1.4.10]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.9..v1.4.10 478 | [1.4.9]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.8..v1.4.9 479 | [1.4.8]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.7..v1.4.8 480 | [1.4.7]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.6..v1.4.7 481 | [1.4.6]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.5..v1.4.6 482 | [1.4.5]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.4..v1.4.5 483 | [1.4.4]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.3..v1.4.4 484 | [1.4.3]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.2..v1.4.3 485 | [1.4.2]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.1..v1.4.2 486 | [1.4.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.4.0..v1.4.1 487 | [1.4.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.7..v1.4.0 488 | [1.3.7]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.6..v1.3.7 489 | [1.3.6]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.5..v1.3.6 490 | [1.3.5]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.4..v1.3.5 491 | [1.3.4]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.3..v1.3.4 492 | [1.3.3]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.2..v1.3.3 493 | [1.3.2]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.1..v1.3.2 494 | [1.3.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.3.0..v1.3.1 495 | [1.3.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.2.0..v1.3.0 496 | [1.2.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.1.0..v1.2.0 497 | [1.1.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.0.5..v1.1.0 498 | [1.0.5]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.0.4..v1.0.5 499 | [1.0.4]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.0.3..v1.0.4 500 | [1.0.3]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.0.2..v1.0.3 501 | [1.0.2]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.0.1..v1.0.2 502 | [1.0.1]: https://github.com/zero-plusplus/vscode-autohotkey-debug/compare/v1.0.0..v1.0.1 503 | [1.0.0]: https://github.com/zero-plusplus/vscode-autohotkey-debug/tree/v1.0.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Translated by [DeepL Tranlator](https://www.deepl.com/translator)** 2 | 3 | **If you encounter problems with v1.11.1, please downgrade to v1.11.0.** 4 | 5 | # IMPORTANT 6 | In order to protect my goal of growing my extension, I have decided to waive the MIT license. This is a statement that I do not want you to use my source code in ways that compete with my activities. Also, please do not include it in extension pack, etc. 7 | 8 | This does not affect end users at all. Please continue to use the end user as before. 9 | 10 | From now on, I have decided to develop an [AutoHotkey IDE](https://github.com/zero-plusplus/autohotkey-devtools) from scratch under a new license. The features of vscode-autohotkey-debug up to v1.11.0 and those planned to be implemented in v2.0.0 will be integrated into this IDE after it is reconfigured. 11 | 12 | I apologize to those of you who have been waiting for v2.0.0 of this extension, please understand. 13 | 14 | Basically, I will cease activity on this extension, but I will continue to respond to [Discussions](https://github.com/zero-plusplus/vscode-autohotkey-debug/discussions) regarding usage, etc. 15 | 16 | # Overview 17 | This extension is a debugger adapter for [VSCode](https://code.visualstudio.com/) that provides many [advanced features](https://github.com/zero-plusplus/vscode-autohotkey-debug/wiki/Features) in addition to the basic debugging features. 18 | 19 | ## Update 20 | * `1.11.1` - 2024-09-23 21 | * Support working with [AutoHotkey Dev Tools](https://github.com/zero-plusplus/autohotkey-devtools) to be released in the future 22 | 23 | There will be no further updates. Previous changes can be found [here](CHANGELOG.md). 24 | 25 | # Installation 26 | 1. Install [VSCode](https://code.visualstudio.com/) with version `1.49.0` or higher 27 | 2. Install [AutoHotkey](https://www.autohotkey.com/) 28 | 3. **Install an another extension to support AutoHotkey** (the famous [slevesque.vscode-autohotkey](https://marketplace.visualstudio.com/items?itemName=slevesque.vscode-autohotkey), If you use v2, use [dudelmoser.vscode-autohotkey2](https://marketplace.visualstudio.com/items?itemName=dudelmoser.vscode-autohotkey2), etc.) 29 | 4. Open VSCode, press `Ctrl + P` then type `ext install zero-plusplus.vscode-autohotkey-debug` 30 | 31 | ## For advanced users 32 | This extension will work without configuration as long as you follow the steps above. 33 | 34 | However, if you want to use a different version of AutoHotkey for which no installer is provided, you will need to configure it separately. 35 | 36 | By default, the runtime is configured for each file extension as shown below, so please place the runtime in the same path. 37 | * `ahk` - `C:/Program Files/AutoHotkey/AutoHotkey.exe` 38 | * `ahk2` or `ah2` - `C:/Program Files/AutoHotkey/v2/AutoHotkey.exe` 39 | 40 | If you want to place the runtime in a specified folder, you need to set the [runtime](https://github.com/zero-plusplus/vscode-autohotkey-debug/wiki/Launch-Mode) attribute in launch.json. 41 | 42 | # Usage 43 | 1. Open a file with the extension `ahk`, `ahk2` or `ah2`. 44 | 2. (optional) Set [Breakpoint](https://github.com/zero-plusplus/vscode-autohotkey-debug/wiki/Breakpoint) where you want them 45 | 3. Press `F5` 46 | 47 | If you want to enable more advanced features and make more detailed settings, please refer to [Debug configurations](https://github.com/zero-plusplus/vscode-autohotkey-debug/wiki/Debug-configurations). 48 | 49 | If you need to run the script without debugging, choose `Run -> Run Without Debugging` from the menu or press `Ctrl + F5`. 50 | 51 | For more information on how to use many of the other features, see [here](https://github.com/zero-plusplus/vscode-autohotkey-debug/wiki). 52 | -------------------------------------------------------------------------------- /demo/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "AutoHotkey Debug", 6 | "type": "autohotkey", 7 | "request": "launch", 8 | "program": "${file}", 9 | "runtime_v1": "AutoHotkey.exe", 10 | "runtime_v2": "v2/AutoHotkey.exe", 11 | "args": [], 12 | "port": "9002-9010", 13 | "stopOnEntry": false, 14 | "useDebugDirective": true, 15 | "usePerfTips": true, 16 | "openFileOnExit": "${file}", 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "useAutoJumpToError": true, 19 | "trace": false, 20 | "useOutputDebug": { 21 | "useTrailingLinebreak": true 22 | }, 23 | "useAnnounce": "detail", 24 | "variableCategories": "recommend" 25 | }, 26 | { 27 | "name": "VariableCategories Test", 28 | "type": "autohotkey", 29 | "request": "launch", 30 | "program": "${file}", 31 | "runtime_v1": "AutoHotkey.exe", 32 | "runtime_v2": "v2/AutoHotkey.exe", 33 | "args": [], 34 | "port": "9002-9010", 35 | "stopOnEntry": false, 36 | "useDebugDirective": true, 37 | "usePerfTips": true, 38 | "openFileOnExit": "${file}", 39 | "internalConsoleOptions": "openOnSessionStart", 40 | "useAutoJumpToError": true, 41 | "trace": false, 42 | "variableCategories": [ 43 | "Local", 44 | { 45 | "label": "Global", 46 | "source": "Global", 47 | "noduplicate": true, 48 | "matchers": [ 49 | { 50 | "method": "exclude", 51 | "pattern": "\\d" 52 | }, 53 | ] 54 | }, 55 | { 56 | "label": "Global Func", 57 | "source": "Global", 58 | "hidden": "auto", 59 | "matchers": [ 60 | { 61 | "method": "exclude", 62 | "pattern": "\\d" 63 | }, 64 | { 65 | "className": "Func", 66 | "builtin": false, 67 | }, 68 | ], 69 | }, 70 | { 71 | "label": "Built-in Func", 72 | "source": "Global", 73 | "hidden": "auto", 74 | "matchers": [ 75 | { 76 | "method": "exclude", 77 | "pattern": "\\d" 78 | }, 79 | { 80 | "className": "Func", 81 | "builtin": true, 82 | }, 83 | ], 84 | }, 85 | { 86 | "label": "Built-in Global", 87 | "source": "Global", 88 | "matchers": [ 89 | { 90 | "method": "exclude", 91 | "pattern": "\\d" 92 | }, 93 | { 94 | "method": "exclude", 95 | "className": "Func", 96 | }, 97 | { 98 | "builtin": true, 99 | } 100 | ], 101 | }, 102 | { 103 | "label": "Constant", 104 | "source": "Global", 105 | "matchers": [ 106 | { 107 | "pattern": "^[A-Z_]+$", 108 | "ignorecase": false, 109 | } 110 | ], 111 | }, 112 | { 113 | "label": "All", 114 | "source": [ "Local", "Static", "Global" ], 115 | }, 116 | ], 117 | "useAnnounce": "detail", 118 | }, 119 | { 120 | "name": "AutoHotkey Debug (UIA)", 121 | "type": "autohotkey", 122 | "request": "launch", 123 | "program": "${file}", 124 | "runtime": "AutoHotkeyU64_UIA.exe", 125 | "args": [], 126 | "port": "9002-9010", 127 | "stopOnEntry": false, 128 | "useDebugDirective": true, 129 | "usePerfTips": true, 130 | "openFileOnExit": "${file}", 131 | "internalConsoleOptions": "openOnSessionStart", 132 | "useAutoJumpToError": true, 133 | "useUIAVersion": true, 134 | "useAnnounce": "detail", 135 | "trace": false, 136 | }, 137 | { 138 | "name": "AutoHotkey_H Debug", 139 | "request": "launch", 140 | "type": "autohotkey", 141 | "runtime_v1": "h/AutoHotkey.exe", 142 | "runtime_v2": "h/v2/AutoHotkey.exe", 143 | "program": "${file}", 144 | "args": [], 145 | "stopOnEntry": false, 146 | "maxChildren": 10000, 147 | "useAnnounce": "detail", 148 | "variableCategories": "recommend", 149 | }, 150 | { 151 | "name": "AutoHotkey Attach", 152 | "type": "autohotkey", 153 | "request": "attach", 154 | "runtime": "AutoHotkey.exe", 155 | "program": "${file}", 156 | "hostname": "127.0.0.1", 157 | "useDebugDirective": true, 158 | "openFileOnExit": "${file}", 159 | "useAutoJumpToError": true, 160 | "useAnnounce": "detail", 161 | "trace": false, 162 | }, 163 | { 164 | "name": "AutoHotkey Selected Attach", 165 | "type": "autohotkey", 166 | "request": "attach", 167 | "runtime": "AutoHotkey.exe", 168 | "hostname": "127.0.0.1", 169 | "useDebugDirective": true, 170 | "openFileOnExit": "${file}", 171 | "useAutoJumpToError": true, 172 | "useAnnounce": "detail", 173 | "stopOnEntry": true, 174 | "trace": false, 175 | }, 176 | ] 177 | } -------------------------------------------------------------------------------- /demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.snippetSuggestions": "none" 3 | } -------------------------------------------------------------------------------- /demo/demo-attach.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance, Force 2 | #Persistent 3 | 4 | SetTimer LoopFn, 1000 5 | LoopFn() { 6 | a := "" 7 | b := "" 8 | c := "" 9 | FileAppend, stdout`n, * 10 | FileAppend, stderr`n, ** 11 | OutputDebug outputdebug`n 12 | } -------------------------------------------------------------------------------- /demo/demo-attach.ahk2: -------------------------------------------------------------------------------- 1 | #SingleInstance 2 | SetTimer LoopFn, 1000 3 | LoopFn() { 4 | a := "" 5 | b := "" 6 | c := "" 7 | FileOpen("*", "w").writeLine("stdout") 8 | FileOpen("**", "w").writeLine("stderr") 9 | OutputDebug("outputdebug") 10 | } -------------------------------------------------------------------------------- /demo/demo-stacktrace.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance Force 2 | a() 3 | a() { 4 | static staticVar := "a" 5 | localVar := A_ThisFunc 6 | b() 7 | } 8 | b() 9 | { 10 | static staticVar := "b" 11 | localVar := A_ThisFunc 12 | c() 13 | } 14 | c() 15 | { 16 | static staticVar := "c" 17 | localVar := A_ThisFunc 18 | d() 19 | } 20 | d() 21 | { 22 | static staticVar := "d" 23 | localVar := A_ThisFunc 24 | e() 25 | } 26 | e() 27 | { 28 | static staticVar := "e" 29 | localVar := A_ThisFunc 30 | f() 31 | } 32 | f() 33 | { 34 | static staticVar := "f" 35 | localVar := A_ThisFunc 36 | g() 37 | } 38 | g() 39 | { 40 | static staticVar := "g" 41 | localVar := A_ThisFunc 42 | h() 43 | } 44 | h() 45 | { 46 | static staticVar := "h" 47 | localVar := A_ThisFunc 48 | i() 49 | } 50 | i() 51 | { 52 | static staticVar := "i" 53 | localVar := A_ThisFunc 54 | j() 55 | } 56 | j() 57 | { 58 | static staticVar := "j" 59 | localVar := A_ThisFunc 60 | k() 61 | } 62 | k() 63 | { 64 | static staticVar := "k" 65 | localVar := A_ThisFunc 66 | l() 67 | } 68 | l() 69 | { 70 | static staticVar := "l" 71 | localVar := A_ThisFunc 72 | m() 73 | } 74 | m() 75 | { 76 | static staticVar := "m" 77 | localVar := A_ThisFunc 78 | n() 79 | } 80 | n() 81 | { 82 | static staticVar := "n" 83 | localVar := A_ThisFunc 84 | o() 85 | } 86 | o() 87 | { 88 | static staticVar := "o" 89 | localVar := A_ThisFunc 90 | p() 91 | } 92 | p() 93 | { 94 | static staticVar := "p" 95 | localVar := A_ThisFunc 96 | q() 97 | } 98 | q() 99 | { 100 | static staticVar := "q" 101 | localVar := A_ThisFunc 102 | r() 103 | } 104 | r() 105 | { 106 | static staticVar := "r" 107 | localVar := A_ThisFunc 108 | s() 109 | } 110 | s() 111 | { 112 | static staticVar := "s" 113 | localVar := A_ThisFunc 114 | t() 115 | } 116 | t() 117 | { 118 | static staticVar := "t" 119 | localVar := A_ThisFunc 120 | u() 121 | } 122 | u() 123 | { 124 | static staticVar := "u" 125 | localVar := A_ThisFunc 126 | v() 127 | } 128 | v() 129 | { 130 | static staticVar := "v" 131 | localVar := A_ThisFunc 132 | w() 133 | } 134 | w() 135 | { 136 | static staticVar := "w" 137 | localVar := A_ThisFunc 138 | x() 139 | } 140 | x() 141 | { 142 | static staticVar := "x" 143 | localVar := A_ThisFunc 144 | y() 145 | } 146 | y() 147 | { 148 | static staticVar := "y" 149 | localVar := A_ThisFunc 150 | z() 151 | } 152 | z() 153 | { 154 | static staticVar := "z" 155 | localVar := A_ThisFunc 156 | } -------------------------------------------------------------------------------- /demo/demo-stacktrace.ahk2: -------------------------------------------------------------------------------- 1 | #Include ./demo-stacktrace.ahk -------------------------------------------------------------------------------- /demo/demo.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance Force 2 | #Warn All, StdOut 3 | globalVar := "Global" 4 | global SuperGlobalVar := "SuperGlobal" 5 | 6 | demo() 7 | demo() 8 | { 9 | static staticVar := "Static" 10 | 11 | ; Overwrite global var 12 | globalVar := "Local" 13 | SuperGlobalVar := "Local" 14 | 15 | ; Primitives 16 | str_empty := "" 17 | str := "string" 18 | str_multiline := " 19 | (LTrim 20 | line 1 21 | line 2 22 | line 3 23 | )" 24 | int := 123 25 | int_like := "123" 26 | int_negative := -123 27 | float := 123.456 28 | float_like := "123.456" 29 | float_negative := -123.456 30 | hex := 0x123 31 | hex_negative := -0x123 32 | scientificNotation := 1.0e5 33 | bool_true := true 34 | bool_false := false 35 | 36 | ; Objects 37 | arr_empty := [] 38 | arr := [ str, int, Util_CreateLargeArray() ] 39 | arr_maxsize := Util_CreateMaxSizeArray() 40 | arr_giant := Util_CreateGiantArray() 41 | arr_like := Util_CreateLargeArray() 42 | arr_like.size := arr_like.length() 43 | arr_sparse := { 1: str, 9000: int } 44 | 45 | obj_empty := {} 46 | obj := { str: str, int: int, arr: arr } 47 | obj_specialkey := { [1, 2, 3]: "value", "spaced key": "value2"} 48 | 49 | circular := {} 50 | circular.circular := circular 51 | 52 | instance := new Clazz() 53 | instance.property := "overwrite" 54 | instance.method() 55 | } 56 | class Clazz extends ClazzBase 57 | { 58 | ; static 59 | static staticField := "static field" 60 | 61 | ; property 62 | _property_baking := "baking" 63 | property[] { 64 | get { 65 | return this._property_baking 66 | } 67 | set { 68 | return this._property_baking := value 69 | } 70 | } 71 | 72 | ; instance field 73 | field := "instance field" 74 | 75 | ; method 76 | method() 77 | { 78 | FileAppend Called method!`n, * 79 | } 80 | } 81 | class ClazzBase 82 | { 83 | baseField := "baseField" 84 | } -------------------------------------------------------------------------------- /demo/demo.ahk2: -------------------------------------------------------------------------------- 1 | #SingleInstance Force 2 | #Include 3 | 4 | globalVar := "Global" 5 | global SuperGlobalVar := "SuperGlobal" 6 | 7 | demo() 8 | demo() 9 | { 10 | static staticVar := "Static" 11 | 12 | ; Overwrite global var 13 | globalVar := "Local" 14 | SuperGlobalVar := "Local" 15 | 16 | ; Primitives 17 | str_empty := "" 18 | str := "string" 19 | str_multiline := " 20 | (LTrim 21 | line 1 22 | line 2 23 | line 3 24 | )" 25 | int := 123 26 | int_like := "123" 27 | int_negative := -123 28 | float := 123.456 29 | float_like := "123.456" 30 | float_negative := -123.456 31 | hex := 0x123 32 | hex_negative := -0x123 33 | scientificNotation := 1e+5 34 | bool_true := true 35 | bool_false := false 36 | 37 | ; Objects 38 | arr_empty := [] 39 | arr := [ str, int, Util_CreateLargeArray() ] 40 | arr_maxsize := Util_CreateMaxSizeArray() 41 | arr_giant := Util_CreateGiantArray() 42 | arr_like := Map() 43 | for i, value in Util_CreateLargeArray() { 44 | arr_like[i] := value 45 | } 46 | arr_like["size"] := arr_like.count 47 | arr_sparse := Map(1, str, 9000, int) 48 | obj_specialkey := Map([ 1, 2, 3 ], "value", "spaced key", "value2") 49 | 50 | obj_empty := {} 51 | obj := { str: str, int: int, arr: arr } 52 | 53 | mp := Map("key", "value", [ 1, 2, 3 ], "value2") 54 | buf := Buffer(10) 55 | 56 | fatArrow := () => "fatArrow" 57 | closure := () => fatArrow 58 | 59 | circular := {} 60 | circular.circular := circular 61 | 62 | instance := Clazz() 63 | instance.property := "overwrite" 64 | instance.method() 65 | } 66 | class Clazz extends ClazzBase 67 | { 68 | static staticField := "static field" 69 | static _staticField_baking := "baking" 70 | static staticProperty { 71 | get { 72 | return this._staticField_baking 73 | } 74 | set { 75 | return this._staticField_baking := value 76 | } 77 | } 78 | __Enum() { 79 | return this.ownProps() 80 | } 81 | 82 | field := "instance field" 83 | _property_baking := "baking" 84 | property { 85 | get { 86 | return this._property_baking 87 | } 88 | set { 89 | return this._property_baking := value 90 | } 91 | } 92 | 93 | ; method 94 | static staticMethod() 95 | { 96 | FileAppend("Called static method!`n", "*") 97 | } 98 | method() 99 | { 100 | FileAppend("Called method!`n", "*") 101 | } 102 | } 103 | class ClazzBase 104 | { 105 | baseField := "baseField" 106 | } -------------------------------------------------------------------------------- /demo/lib/Util.ahk: -------------------------------------------------------------------------------- 1 | Util_CreateLargeArray() 2 | { 3 | arr := [] 4 | Loop 1000 5 | { 6 | arr.push(A_Index) 7 | } 8 | return arr 9 | } 10 | Util_CreateGiantArray() 11 | { 12 | arr := [] 13 | Loop 500 14 | { 15 | arr.push(Util_CreateLargeArray()) 16 | } 17 | return arr 18 | } 19 | Util_CreateMaxSizeArray() 20 | { 21 | arr := [] 22 | Loop 10000 23 | { 24 | arr.push(A_Index) 25 | } 26 | return arr 27 | } -------------------------------------------------------------------------------- /gulpfile.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import glob from 'fast-glob'; 3 | import run from 'gulp-run-command'; 4 | import del from 'del'; 5 | import * as esbuild from 'esbuild'; 6 | import { ESLint } from 'eslint'; 7 | import * as fs from 'fs-extra'; 8 | import * as path from 'path'; 9 | import tsconfig from './tsconfig.json'; 10 | import eslintrc from './.eslintrc'; 11 | import { isDirectory } from './src/util/util'; 12 | import { spawn } from 'child_process'; 13 | 14 | const clean = async(): Promise => { 15 | await del('./build'); 16 | }; 17 | 18 | const tscheck = async(): Promise => { 19 | await run('tsc --noEmit')(); 20 | }; 21 | const eslint = async(): Promise => { 22 | const eslint = new ESLint({ baseConfig: eslintrc, fix: true, cache: true }); 23 | const results = await eslint.lintFiles(tsconfig.include).catch((err) => { 24 | throw err; 25 | }); 26 | await ESLint.outputFixes(results); 27 | }; 28 | // #region build 29 | const esbuildCommonOptions: esbuild.BuildOptions = { 30 | platform: 'node', 31 | format: 'cjs', 32 | }; 33 | const esbuildOptions: esbuild.BuildOptions = { 34 | ...esbuildCommonOptions, 35 | entryPoints: [ './src/extension.ts' ], 36 | outfile: './build/extension.js', 37 | bundle: true, 38 | minify: true, 39 | treeShaking: true, 40 | external: [ 41 | 'ts-predicates', 42 | 'typeof-util', 43 | 'vscode-uri', 44 | 'vscode', 45 | ], 46 | }; 47 | const esbuildDebugOptions: esbuild.BuildOptions = { 48 | ...esbuildCommonOptions, 49 | entryPoints: glob.sync('./src/**/*.ts'), 50 | outdir: 'build', 51 | sourcemap: true, 52 | }; 53 | const buildMain = async(): Promise => { 54 | await esbuild.build(esbuildOptions); 55 | }; 56 | const buildMainDebug = async(): Promise => { 57 | await esbuild.build(esbuildDebugOptions); 58 | }; 59 | // #endregion build 60 | 61 | const lint = gulp.parallel(tscheck, eslint); 62 | const runSandBoxTest = async(): Promise => { 63 | const packageJson = JSON.parse(await fs.readFile(`${__dirname}/package.json`, 'utf-8')); 64 | const packageName = String(packageJson.name); 65 | const packageVersion = String(packageJson.version); 66 | const tempDir = path.resolve(`${String(process.env.USERPROFILE)}/AppData/Local/Temp`); 67 | const wsbDirPath = path.resolve(`${tempDir}/${String(packageJson.name)}`); 68 | const wsbPath = path.resolve(`${wsbDirPath}/sandbox.wsb`); 69 | 70 | if (isDirectory(wsbDirPath)) { 71 | await del(wsbDirPath, { force: true }); 72 | } 73 | await fs.mkdir(wsbDirPath); 74 | await fs.writeFile(wsbPath, ` 75 | 76 | 77 | 78 | ${wsbDirPath} 79 | 80 | 81 | 82 | powershell -executionpolicy unrestricted -command "start powershell { -noexit -file C:\\Users\\WDAGUtilityAccount\\Desktop\\${packageName}\\installer.ps1 }" 83 | 84 | 85 | `); 86 | 87 | await fs.copyFile(`${__dirname}/${packageName}-${packageVersion}.vsix`, `${wsbDirPath}/${packageName}.vsix`); 88 | await fs.copy(`${__dirname}/demo`, `${wsbDirPath}/demo`); 89 | const installerPath = `${wsbDirPath}/installer.ps1`; 90 | await fs.writeFile(installerPath, ` 91 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 92 | $env:ChocolateyInstall = Convert-Path "$((Get-Command choco).path)\\..\\.." 93 | Import-Module "$env:ChocolateyInstall\\helpers\\chocolateyProfile.psm1" 94 | 95 | choco install vscode -y 96 | choco install autohotkey -y 97 | # Install autohotkey2 98 | Invoke-RestMethod -Uri "https://www.autohotkey.com/download/ahk-v2.zip" -OutFile "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey2.zip" 99 | Expand-Archive -Path "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey2.zip" -DestinationPath "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey2" 100 | New-Item "C:\\Program Files\\AutoHotkey\\v2" -ItemType Directory 101 | Copy-Item -Path "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey2\\AutoHotkey64.exe" -Destination "C:\\Program Files\\AutoHotkey\\v2\\AutoHotkey.exe" 102 | 103 | refreshenv 104 | code --install-extension slevesque.vscode-autohotkey 105 | code --install-extension dudelmoser.vscode-autohotkey2 106 | code --install-extension C:\\Users\\WDAGUtilityAccount\\Desktop\\${packageName}\\${packageName}.vsix 107 | code C:\\Users\\WDAGUtilityAccount\\Desktop\\${packageName}\\demo 108 | 109 | # The following will be installed last due to slow processing 110 | # Install autohotkey_h 111 | Invoke-RestMethod -Uri "https://github.com/HotKeyIt/ahkdll-v1-release/archive/master.zip" -OutFile "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h.zip" 112 | Expand-Archive -Path "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h.zip" -DestinationPath "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h" 113 | New-Item "C:\\Program Files\\AutoHotkey\\h" -ItemType Directory 114 | Copy-Item -Path "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h\\ahkdll-v1-release-master\\x64w_MT\\AutoHotkey.exe" -Destination "C:\\Program Files\\AutoHotkey\\h\\AutoHotkey.exe" 115 | 116 | # Install autohotkey_h2 117 | Invoke-RestMethod -Uri "https://github.com/HotKeyIt/ahkdll-v2-release/archive/master.zip" -OutFile "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h2.zip" 118 | Expand-Archive -Path "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h2.zip" -DestinationPath "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h2" 119 | New-Item "C:\\Program Files\\AutoHotkey\\h\\v2" -ItemType Directory 120 | Copy-Item -Path "C:\\Users\\WDAGUtilityAccount\\Downloads\\autohotkey_h2\\ahkdll-v2-release-master\\x64w_MT\\AutoHotkey.exe" -Destination "C:\\Program Files\\AutoHotkey\\h\\v2\\AutoHotkey.exe" 121 | 122 | exit 123 | `); 124 | spawn('WindowsSandBox', [ wsbPath ], { detached: true }); 125 | }; 126 | 127 | const buildWithoutClean = gulp.parallel(lint, buildMain); 128 | const build = gulp.series(clean, buildWithoutClean); 129 | const vscePackage = async(): Promise => { 130 | await run('npx @vscode/vsce package')(); 131 | }; 132 | const watchMain = async(): Promise => { 133 | await esbuild.build({ 134 | ...esbuildDebugOptions, 135 | incremental: true, 136 | watch: { 137 | onRebuild: (err, result) => { 138 | if (err) { 139 | console.log(`[esbuild] error: ${err.message}`); 140 | } 141 | console.log(`[esbuild] build completed`); 142 | }, 143 | }, 144 | }); 145 | }; 146 | const watch = gulp.series(clean, watchMain); 147 | const packaging = gulp.series(clean, gulp.parallel(lint, vscePackage)); 148 | const testBySandBox = gulp.series(packaging, runSandBoxTest); 149 | 150 | export { 151 | build, 152 | buildWithoutClean, 153 | buildMain, 154 | buildMainDebug, 155 | runSandBoxTest, 156 | testBySandBox, 157 | watch, 158 | watchMain, 159 | packaging, 160 | clean, 161 | lint, 162 | tscheck, 163 | eslint, 164 | }; 165 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/icon.png -------------------------------------------------------------------------------- /image/breakpoint-directive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/breakpoint-directive.gif -------------------------------------------------------------------------------- /image/call-stack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/call-stack.gif -------------------------------------------------------------------------------- /image/conditional-breakpoint.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/conditional-breakpoint.gif -------------------------------------------------------------------------------- /image/data-inspection-when-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/data-inspection-when-hover.gif -------------------------------------------------------------------------------- /image/data-inspection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/data-inspection.gif -------------------------------------------------------------------------------- /image/debug-console.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/debug-console.jpg -------------------------------------------------------------------------------- /image/hit-conditional-breakpoint.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/hit-conditional-breakpoint.gif -------------------------------------------------------------------------------- /image/intellisense.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/intellisense.gif -------------------------------------------------------------------------------- /image/loaded-scripts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/loaded-scripts.gif -------------------------------------------------------------------------------- /image/log-point.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/log-point.gif -------------------------------------------------------------------------------- /image/output-directive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/output-directive.gif -------------------------------------------------------------------------------- /image/perftips.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/perftips.gif -------------------------------------------------------------------------------- /image/rewriting-variables.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/rewriting-variables.gif -------------------------------------------------------------------------------- /image/watch-expression.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plusplus/vscode-autohotkey-debug/c687a78eb27c2557a7d85e81a5263f0c2570a9df/image/watch-expression.gif -------------------------------------------------------------------------------- /src/CompletionItemProvider.ts: -------------------------------------------------------------------------------- 1 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 2 | import { count } from 'underscore.string'; 3 | import * as vscode from 'vscode'; 4 | import * as dbgp from './dbgpSession'; 5 | import { lastIndexOf } from './util/stringUtils'; 6 | import { splitVariablePath } from './util/util'; 7 | 8 | export interface CompletionItemProvider extends vscode.CompletionItemProvider { 9 | useIntelliSenseInDebugging: boolean; 10 | session: dbgp.Session | null; 11 | } 12 | 13 | const createKind = (property: dbgp.Property): vscode.CompletionItemKind => { 14 | let kind: vscode.CompletionItemKind = property.fullName.includes('.') 15 | ? vscode.CompletionItemKind.Field 16 | : vscode.CompletionItemKind.Variable; 17 | if (property instanceof dbgp.ObjectProperty) { 18 | if (property.className === 'Func') { 19 | if (property.fullName.includes('.')) { 20 | kind = vscode.CompletionItemKind.Method; 21 | } 22 | else { 23 | kind = vscode.CompletionItemKind.Function; 24 | } 25 | } 26 | else if (property.className === 'Class') { 27 | kind = vscode.CompletionItemKind.Class; 28 | } 29 | else if (property.className === 'Property') { 30 | kind = vscode.CompletionItemKind.Property; 31 | } 32 | else if (property.name.toLowerCase() === '__new') { 33 | kind = vscode.CompletionItemKind.Constructor; 34 | } 35 | } 36 | return kind; 37 | }; 38 | const createDetail = (property: dbgp.Property): string => { 39 | const kindName = vscode.CompletionItemKind[createKind(property)].toLowerCase(); 40 | const context = property.fullName.includes('.') || property.fullName.includes('[') 41 | ? '' 42 | : `[${property.context.name}] `; 43 | if (kindName === 'class') { 44 | return `${context}(${kindName}) ${(property as dbgp.ObjectProperty).className}`; 45 | } 46 | 47 | const type = property instanceof dbgp.ObjectProperty ? property.className : property.type; 48 | return `${context}(${kindName}) ${property.fullName}: ${type}`; 49 | }; 50 | 51 | export const fixPosition = (position: vscode.Position, offset: number): vscode.Position => { 52 | return new vscode.Position(position.line, Math.max(position.character + offset, 0)); 53 | }; 54 | export const findWord = (ahkVersion: AhkVersion, document: vscode.TextDocument, position: vscode.Position, offset = 0): string => { 55 | const targetText = document.lineAt(position).text.slice(0, fixPosition(position, offset).character); 56 | const chars = targetText.split('').reverse(); 57 | const openBracketIndex = lastIndexOf(targetText, 2 <= ahkVersion.mejor ? /(?<=\[\s*)("|')/u : /(?<=\[\s*)(")/u); 58 | const closeBracketIndex = lastIndexOf(targetText, 2 <= ahkVersion.mejor ? /("|')\s*(?=\])/u : /"\s*(?=\])/u); 59 | 60 | const result: string[] = []; 61 | let quote = '', bracketCount = 0; 62 | if (-1 < openBracketIndex && closeBracketIndex < openBracketIndex) { 63 | quote = targetText.charAt(openBracketIndex); 64 | bracketCount = 1; 65 | } 66 | for (let i = 0; i < chars.length; i++) { 67 | const char = chars[i]; 68 | const nextChar = chars[i + 1] as string | undefined; 69 | 70 | if (quote) { 71 | if (2 < ahkVersion.mejor) { 72 | if (char === '"' && nextChar === '`') { 73 | result.push(char); 74 | result.push('"'); 75 | i++; 76 | continue; 77 | } 78 | 79 | if (quote === `'` && char === '"') { 80 | result.push('"'); 81 | result.push('"'); 82 | continue; 83 | } 84 | else if (quote === `'` && char === `'`) { 85 | result.push('"'); 86 | quote = ''; 87 | continue; 88 | } 89 | } 90 | 91 | if (quote === char) { 92 | result.push(quote); 93 | quote = ''; 94 | continue; 95 | } 96 | result.push(char); 97 | continue; 98 | } 99 | else if (0 < bracketCount && char === '[') { 100 | result.push(char); 101 | bracketCount--; 102 | continue; 103 | } 104 | 105 | switch (char) { 106 | case `'`: { 107 | if (2 <= ahkVersion.mejor) { 108 | quote = char; 109 | result.push(quote); 110 | continue; 111 | } 112 | result.push(char); 113 | continue; 114 | } 115 | case '"': { 116 | quote = char; 117 | result.push(char); 118 | continue; 119 | } 120 | case ']': { 121 | result.push(char); 122 | bracketCount++; 123 | continue; 124 | } 125 | case '.': { 126 | result.push(char); 127 | continue; 128 | } 129 | default: { 130 | const isIdentifierChar = (2 <= ahkVersion.mejor ? /[\w_]/u : /[\w_$@#]/u).test(char); 131 | if (isIdentifierChar) { 132 | result.push(char); 133 | continue; 134 | } 135 | 136 | // Skip spaces 137 | if (0 < bracketCount && (/\s/u).test(char)) { 138 | continue; 139 | } 140 | } 141 | } 142 | break; 143 | } 144 | return result.reverse().join(''); 145 | }; 146 | export const completionItemProvider = { 147 | useIntelliSenseInDebugging: true, 148 | session: null, 149 | async provideCompletionItems(document, position, token, context) { 150 | if (!this.useIntelliSenseInDebugging) { 151 | return []; 152 | } 153 | if (!this.session) { 154 | return []; 155 | } 156 | else if (this.session.socketClosed) { 157 | this.session = null; 158 | return []; 159 | } 160 | 161 | // #region util 162 | const getPrevText = (length: number): string => { 163 | return document.getText(new vscode.Range(fixPosition(position, -length), position)); 164 | }; 165 | // #endregion util 166 | 167 | const word = findWord(this.session.ahkVersion, document, position); 168 | const lastWordPathPart = splitVariablePath(this.session.ahkVersion, word).pop() ?? ''; 169 | const isBracketNotation = lastWordPathPart.startsWith('['); 170 | const triggerCharacter = context.triggerCharacter ?? getPrevText(1); 171 | 172 | const properties = await this.session.fetchSuggestList(word); 173 | const fixedProperties = properties.filter((property) => { 174 | if (property.name === '') { 175 | return false; 176 | } 177 | if ((/\d+/u).test(property.name)) { 178 | return false; 179 | } 180 | if (property.isIndexKey) { 181 | return false; 182 | } 183 | const isIndexKeyByObject = (/[\w]+\(\d+\)/ui).test(property.name); 184 | if (isIndexKeyByObject) { 185 | return false; 186 | } 187 | return true; 188 | }); 189 | 190 | const completionItems = fixedProperties.map((property): vscode.CompletionItem => { 191 | const completionItem = new vscode.CompletionItem(property.name.replace(/<|>/gu, '')); 192 | completionItem.kind = createKind(property); 193 | completionItem.detail = createDetail(property); 194 | 195 | const depth = count(property.fullName, '.'); 196 | const priority = property.name.startsWith('__') ? 1 : 0; 197 | completionItem.sortText = `@:${priority}:${depth}:${property.name}`; 198 | 199 | if (property.name.startsWith('[')) { 200 | const fixQuote = (str: string): string => { 201 | return str.replace(/""/gu, '`"'); 202 | }; 203 | const fixLabel = (label: string): string => { 204 | if (2 <= this.session!.ahkVersion.mejor) { 205 | const fixedLabel = label.replace(/^\[("|')?/u, '').replace(/("|')?\]$/u, ''); 206 | return fixQuote(fixedLabel); 207 | } 208 | const fixedLabel = label.replace(/^\[(")?/u, '').replace(/(")?\]$/u, ''); 209 | return fixedLabel; 210 | }; 211 | 212 | const fixedLabel = fixLabel(property.name); 213 | completionItem.label = fixedLabel; 214 | 215 | if (!isBracketNotation && triggerCharacter === '.') { 216 | // Since I could not use the range property to replace the dots as shown below, I will use TextEdit 217 | // completionItem.range = { 218 | // inserting: new vscode.Range(fixedPosition, fixedPosition), 219 | // replacing: new vscode.Range(fixedPosition, position), 220 | // }; 221 | 222 | // This is a strange implementation because I don't know the specs very well. 223 | const replaceRange = new vscode.Range(fixPosition(position, -1), position); 224 | const fixedPropertyName = fixQuote(property.name); 225 | const a = fixedPropertyName.slice(0, 1); 226 | const b = fixedPropertyName.slice(1); 227 | completionItem.additionalTextEdits = [ new vscode.TextEdit(replaceRange, a) ]; 228 | completionItem.insertText = b; 229 | } 230 | else if (lastWordPathPart.startsWith('[')) { 231 | completionItem.insertText = fixLabel(property.name); // fixedLabel.slice(fixedLastWordPathPart.length); 232 | 233 | const fixedLastWordPathPart = fixLabel(lastWordPathPart); 234 | completionItem.range = new vscode.Range(fixPosition(position, -fixedLastWordPathPart.length), position); 235 | } 236 | else { 237 | completionItem.insertText = fixedLabel; 238 | } 239 | } 240 | 241 | return completionItem; 242 | }); 243 | 244 | return completionItems; 245 | }, 246 | } as CompletionItemProvider; 247 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DebugProtocol } from 'vscode-debugprotocol'; 3 | import { URI } from 'vscode-uri'; 4 | import { unescapeAhk } from './util/VariableManager'; 5 | 6 | interface VariableContextMenuParam { 7 | container: DebugProtocol.Scope; 8 | variable: DebugProtocol.Variable; 9 | } 10 | const removeQuote = (string: string): string => { 11 | return string.replace(/(^"|"$)/gu, ''); 12 | }; 13 | const convertToText = (param: VariableContextMenuParam): string => { 14 | return unescapeAhk(removeQuote(param.variable.value)); 15 | }; 16 | const convertToBinary = (param: VariableContextMenuParam): string => { 17 | return Math.ceil(Number(removeQuote(param.variable.value))).toString(2); 18 | }; 19 | const convertToDecimal = (param: VariableContextMenuParam): string => { 20 | return Number(removeQuote(param.variable.value)).toString(10); 21 | }; 22 | const convertToHex = (param: VariableContextMenuParam): string => { 23 | const number = Math.ceil(Number(removeQuote(param.variable.value))); 24 | return 0 < number ? `0x${number.toString(16)}` : `-0x${number.toString(16).replace(/^-/u, '')}`; 25 | }; 26 | const convertToScientificNotation = (param: VariableContextMenuParam): string => { 27 | return Number(removeQuote(param.variable.value)).toExponential(); 28 | }; 29 | const showValue = async(text: string): Promise => { 30 | const uri = URI.parse(`valuepreview:${encodeURI(text)}.ahk`); 31 | const doc = await vscode.workspace.openTextDocument(uri); // calls back into the provider 32 | await vscode.window.showTextDocument(doc, { preview: true }); 33 | }; 34 | export const registerCommands = (context: vscode.ExtensionContext): void => { 35 | context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('valuepreview', new class implements vscode.TextDocumentContentProvider { 36 | public readonly onDidChangeEmitter = new vscode.EventEmitter(); 37 | public readonly onDidChange = this.onDidChangeEmitter.event; 38 | public provideTextDocumentContent(uri: vscode.Uri): string { 39 | return decodeURI(uri.path.replace(/\.ahk/u, '')); 40 | } 41 | }())); 42 | 43 | // View 44 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.viewValue', async(param: VariableContextMenuParam): Promise => { 45 | await showValue(param.variable.value); 46 | })); 47 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.viewAsText', async(param: VariableContextMenuParam): Promise => { 48 | await showValue(convertToText(param)); 49 | })); 50 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.ViewAsEachBaseNumbers', async(param: VariableContextMenuParam): Promise => { 51 | const text = [ 52 | `[Binary]`, 53 | convertToBinary(param), 54 | '', 55 | `[Decimal]`, 56 | convertToDecimal(param), 57 | '', 58 | `[Hex]`, 59 | convertToHex(param), 60 | '', 61 | `[Scientific Notation]`, 62 | convertToScientificNotation(param), 63 | ].join('\n'); 64 | await showValue(text); 65 | })); 66 | 67 | // Copy 68 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.copyAsText', async(param: VariableContextMenuParam): Promise => { 69 | const text = convertToText(param); 70 | await vscode.env.clipboard.writeText(text); 71 | })); 72 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.copyAsDecimal', async(param: VariableContextMenuParam): Promise => { 73 | const decimal = convertToDecimal(param); 74 | await vscode.env.clipboard.writeText(decimal); 75 | })); 76 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.copyAsBinary', async(param: VariableContextMenuParam): Promise => { 77 | const binary = convertToBinary(param); 78 | await vscode.env.clipboard.writeText(binary); 79 | })); 80 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.copyAsHex', async(param: VariableContextMenuParam): Promise => { 81 | const hex = convertToHex(param); 82 | await vscode.env.clipboard.writeText(hex); 83 | })); 84 | context.subscriptions.push(vscode.commands.registerCommand('vscode-autohotkey-debug.variables-view.copyAsScientificNotation', async(param: VariableContextMenuParam): Promise => { 85 | const scientificNotation = convertToScientificNotation(param); 86 | await vscode.env.clipboard.writeText(scientificNotation); 87 | })); 88 | }; 89 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-atomic-updates */ 2 | import { existsSync, readFileSync } from 'fs'; 3 | import * as path from 'path'; 4 | import * as vscode from 'vscode'; 5 | import { isArray, isBoolean, isPlainObject } from 'ts-predicates'; 6 | import { defaults, groupBy, isString, range } from 'lodash'; 7 | import isPortTaken from 'is-port-taken'; 8 | import * as jsonc from 'jsonc-simple-parser'; 9 | import { getAhkVersion } from './util/getAhkVersion'; 10 | import { completionItemProvider, findWord } from './CompletionItemProvider'; 11 | import { AhkDebugSession } from './ahkDebug'; 12 | import { getRunningAhkScriptList } from './util/getRunningAhkScriptList'; 13 | import normalizeToUnix from 'normalize-path'; 14 | import glob from 'fast-glob'; 15 | import { registerCommands } from './commands'; 16 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 17 | import { isDirectory, toArray } from './util/util'; 18 | 19 | const ahkPathResolve = (filePath: string, cwd?: string): string => { 20 | let _filePath = filePath; 21 | if (!path.isAbsolute(filePath)) { 22 | _filePath = path.resolve(cwd ?? `${String(process.env.PROGRAMFILES)}/AutoHotkey`, filePath); 23 | } 24 | if (path.extname(_filePath) === '') { 25 | _filePath += '.exe'; 26 | } 27 | return _filePath; 28 | }; 29 | const normalizePath = (filePath: string): string => (filePath ? path.normalize(filePath) : filePath); // If pass in an empty string, path.normalize returns '.' 30 | 31 | export type ScopeName = 'Local' | 'Static' | 'Global'; 32 | export type ScopeSelector = '*' | ScopeName; 33 | export type MatcherData = { 34 | method?: 'include' | 'exclude'; 35 | ignorecase?: boolean; 36 | pattern?: string; 37 | static?: boolean; 38 | builtin?: boolean; 39 | type?: string; 40 | className?: string; 41 | }; 42 | export type CategoryData = { 43 | label: string; 44 | source: ScopeSelector | ScopeName[]; 45 | hidden?: boolean | 'auto'; 46 | noduplicate?: boolean; 47 | matchers?: MatcherData[]; 48 | }; 49 | export type CategoriesData = 'recommend' | Array; 50 | 51 | const normalizeCategories = (categories?: CategoriesData): CategoryData[] | undefined => { 52 | if (!categories) { 53 | return undefined; 54 | } 55 | if (categories === 'recommend') { 56 | return [ 57 | { 58 | label: 'Local', 59 | source: 'Local', 60 | }, 61 | { 62 | label: 'Static', 63 | source: 'Static', 64 | }, 65 | { 66 | label: 'Global', 67 | source: 'Global', 68 | noduplicate: true, 69 | matchers: [ { method: 'exclude', pattern: '^\\d+$' } ], 70 | }, 71 | { 72 | label: 'Built-in Global', 73 | source: 'Global', 74 | matchers: [ 75 | { builtin: true }, 76 | { method: 'exclude', pattern: '^\\d+$' }, 77 | ], 78 | }, 79 | ]; 80 | } 81 | 82 | const normalized: CategoryData[] = []; 83 | for (const category of categories) { 84 | if (typeof category !== 'string') { 85 | normalized.push(category); 86 | } 87 | 88 | switch (category) { 89 | case 'Global': { 90 | normalized.push({ 91 | label: 'Global', 92 | source: 'Global', 93 | }); 94 | continue; 95 | } 96 | case 'Local': { 97 | normalized.push({ 98 | label: 'Local', 99 | source: 'Local', 100 | }); 101 | continue; 102 | } 103 | case 'Static': { 104 | normalized.push({ 105 | label: 'Static', 106 | source: 'Static', 107 | }); 108 | continue; 109 | } 110 | default: continue; 111 | } 112 | } 113 | 114 | const checkNoduplicate = (categoriesData: CategoryData[]): void => { 115 | const groupedCategoriesData = Object.entries(groupBy(categoriesData, (categoryData) => JSON.stringify(toArray(categoryData.source).sort((a, b) => a.localeCompare(b))))); 116 | groupedCategoriesData; 117 | for (const [ , categoriesDataBySource ] of groupedCategoriesData) { 118 | const categoriesWithNoduplicate = categoriesDataBySource.filter((categoryData) => categoryData.noduplicate); 119 | if (categoriesWithNoduplicate.length === 0 || categoriesWithNoduplicate.length === 1) { 120 | continue; 121 | } 122 | 123 | const source = JSON.stringify(categoriesWithNoduplicate[0].source); 124 | throw Error(`There are multiple \`noduplicate\` attributes set for the category with \`${source}\` as the source. This attribute can only be set to one of the categories that have the same source.`); 125 | } 126 | }; 127 | checkNoduplicate(normalized); 128 | return normalized; 129 | }; 130 | 131 | class AhkConfigurationProvider implements vscode.DebugConfigurationProvider { 132 | public resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { 133 | if (config.extends && folder) { 134 | const jsonPath = path.resolve(folder.uri.fsPath, '.vscode', 'launch.json'); 135 | if (existsSync(jsonPath)) { 136 | const launchJson = jsonc.parse(readFileSync(jsonPath, { encoding: 'utf-8' })); 137 | const extendConfig = launchJson.configurations.find((conf) => conf.name === config.extends); 138 | if (!extendConfig) { 139 | throw Error(`No matching configuration found. Please modify the \`extends\` attribute. \nSpecified: ${String(config.extends)}`); 140 | } 141 | defaults(config, extendConfig); 142 | } 143 | } 144 | 145 | defaults(config, { 146 | name: 'AutoHotkey Debug', 147 | type: 'autohotkey', 148 | request: 'launch', 149 | runtime_v1: 'AutoHotkey.exe', 150 | runtime_v2: 'v2/AutoHotkey.exe', 151 | hostname: 'localhost', 152 | port: 9002, 153 | program: config.request === 'launch' || !config.request ? '${file}' : undefined, 154 | args: [], 155 | env: {}, 156 | stopOnEntry: false, 157 | maxChildren: 10000, 158 | useIntelliSenseInDebugging: true, 159 | usePerfTips: false, 160 | useDebugDirective: false, 161 | useAutoJumpToError: false, 162 | useOutputDebug: true, 163 | useUIAVersion: false, 164 | useAnnounce: true, 165 | useLoadedScripts: true, 166 | trace: false, 167 | // The following is not a configuration, but is set to pass data to the debug adapter. 168 | cancelReason: undefined, 169 | }); 170 | 171 | // Deprecated. I''ll get rid of it eventually 172 | if (config.type === 'ahk') { 173 | vscode.window.showErrorMessage('As of version 1.3.7, the `type` of launch.json has been changed from `ahk` to ` It has been changed to `autohotkey`. Please edit launch.json now. If you do not edit it, you will not be able to debug it in the future.'); 174 | config.type = 'autohotkey'; 175 | } 176 | 177 | if (config.openFileOnExit === '${file}' && !vscode.window.activeTextEditor) { 178 | config.openFileOnExit = undefined; 179 | } 180 | return config; 181 | } 182 | public async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise { 183 | // init request 184 | ((): void => { 185 | if (config.request === 'launch') { 186 | return; 187 | } 188 | if (config.request === 'attach') { 189 | return; 190 | } 191 | 192 | throw Error('`request` must be "launch" or "attach".'); 193 | })(); 194 | 195 | // init runtime 196 | await (async(): Promise => { 197 | if (typeof config.runtime === 'undefined') { 198 | const doc = await vscode.workspace.openTextDocument(config.program ?? vscode.window.activeTextEditor?.document.uri.fsPath); 199 | switch (doc.languageId.toLowerCase()) { 200 | case 'ahk': 201 | case 'autohotkeyl': config.runtime = config.runtime_v1; break; 202 | case 'ahk2': 203 | case 'ah2': 204 | case 'autohotkey': 205 | case 'autohotkey2': 206 | case 'autohotkeynext': 207 | default: config.runtime = config.runtime_v2; break; 208 | } 209 | } 210 | 211 | if (!isString(config.runtime)) { 212 | throw Error('`runtime` must be a string.'); 213 | } 214 | if (config.runtime) { 215 | config.runtime = ahkPathResolve(config.runtime); 216 | } 217 | 218 | if (!existsSync(config.runtime)) { 219 | throw Error(`\`runtime\` must be a file path that exists.\nSpecified: "${String(normalizePath(config.runtime))}"`); 220 | } 221 | })(); 222 | 223 | // init useUIAVersion 224 | ((): void => { 225 | if (!isBoolean(config.useUIAVersion)) { 226 | throw Error('`useUIAVersion` must be a boolean.'); 227 | } 228 | })(); 229 | 230 | // init runtimeArgs 231 | await (async(): Promise => { 232 | if (config.useUIAVersion) { 233 | if (!config.runtimeArgs) { 234 | config.runtimeArgs = []; 235 | } 236 | return; 237 | } 238 | else if (typeof config.runtimeArgs === 'undefined') { 239 | const ahkVersion = getAhkVersion(config.runtime, { env: config.env }); 240 | if (ahkVersion === null) { 241 | throw Error(`\`runtime\` is not AutoHotkey runtime.\nSpecified: "${String(normalizePath(config.runtime))}"`); 242 | } 243 | 244 | if (typeof config.runtimeArgs_v1 === 'undefined') { 245 | config.runtimeArgs_v1 = ahkVersion.mejor <= 1.1 && ahkVersion.minor === 1 && 33 <= ahkVersion.patch 246 | ? [ '/ErrorStdOut=UTF-8' ] 247 | : [ '/ErrorStdOut' ]; 248 | } 249 | if (typeof config.runtimeArgs_v2 === 'undefined') { 250 | config.runtimeArgs_v2 = 112 <= (ahkVersion.alpha ?? 0) || 0 < (ahkVersion.beta ?? 0) 251 | ? [ '/ErrorStdOut=UTF-8' ] 252 | : [ '/ErrorStdOut' ]; 253 | } 254 | 255 | const doc = await vscode.workspace.openTextDocument(config.program); 256 | switch (doc.languageId.toLowerCase()) { 257 | case 'ahk': 258 | case 'autohotkeyl': config.runtimeArgs = config.runtimeArgs_v1; break; 259 | case 'ahk2': 260 | case 'ah2': 261 | case 'autohotkey': 262 | case 'autohotkey2': 263 | case 'autohotkeynext': 264 | default: config.runtimeArgs = config.runtimeArgs_v2; break; 265 | } 266 | 267 | config.runtimeArgs = config.runtimeArgs.filter((arg) => arg.search(/\/debug/ui) === -1); 268 | config.runtimeArgs = config.runtimeArgs.filter((arg) => arg !== ''); // If a blank character is set here, AutoHotkey cannot be started. It is confusing for users to pass an empty string as an argument and generate an error, so fix it here. 269 | } 270 | 271 | if (isArray(config.runtimeArgs)) { 272 | return; 273 | } 274 | throw Error('`runtimeArgs` must be a array.'); 275 | })(); 276 | 277 | // init hostname 278 | ((): void => { 279 | if (!isString(config.hostname)) { 280 | throw Error('`hostname` must be a string.'); 281 | } 282 | if (config.hostname.toLowerCase() === 'localhost') { 283 | config.hostname = '127.0.0.1'; 284 | } 285 | })(); 286 | 287 | // init port 288 | await (async(): Promise => { 289 | const portRange = ((): { permitted: boolean; range: number[] } => { 290 | const createUnPermittedPortRange = (port: number): { permitted: boolean; range: number[] } => { 291 | return { permitted: false, range: range(port, port + 100) }; 292 | }; 293 | 294 | if (Number.isInteger(config.port)) { 295 | return createUnPermittedPortRange(config.port as number); 296 | } 297 | else if (typeof config.port === 'string') { 298 | if (config.port.match(/^\d+$/u)) { 299 | return createUnPermittedPortRange(parseInt(config.port, 10)); 300 | } 301 | 302 | const errorMessage = 'It must be specified in the format of "start-last". It must be start < last. e.g. "9002-9010"'; 303 | const match = config.port.match(/^(?\d+)-(?\d+)$/u); 304 | if (!match) { 305 | throw Error(errorMessage); 306 | } 307 | if (!match.groups) { 308 | throw Error(errorMessage); 309 | } 310 | 311 | const start = parseInt(match.groups.start, 10); 312 | const last = parseInt(match.groups.last, 10); 313 | if (isNaN(start) || isNaN(last)) { 314 | throw Error(errorMessage); 315 | } 316 | if (start === last) { 317 | throw Error(errorMessage); 318 | } 319 | if (last <= start) { 320 | throw Error(errorMessage); 321 | } 322 | return { permitted: true, range: range(start, last + 1) }; 323 | } 324 | 325 | throw Error('`port` must be a number or a string of `start-last` format. e.g. "9002-9010"'); 326 | })(); 327 | 328 | for await (const port of portRange.range) { 329 | const portUsed = await isPortTaken(port, config.hostname); 330 | if (!portUsed) { 331 | config.port = port; 332 | return; 333 | } 334 | if (!portRange.permitted) { 335 | const message = `Port number \`${port}\` is already in use. Would you like to start debugging using \`${port + 1}\`?\n If you don't want to see this message, set a value for \`port\` of \`launch.json\`.`; 336 | const result = await vscode.window.showInformationMessage(message, { modal: true }, 'Yes'); 337 | if (!result) { 338 | break; 339 | } 340 | } 341 | } 342 | 343 | throw Error('`port` must be an unused port number.'); 344 | })(); 345 | 346 | // init program 347 | await (async(): Promise => { 348 | if (config.request === 'attach') { 349 | const scriptPathList = getRunningAhkScriptList(config.runtime); 350 | if (scriptPathList.length === 0) { 351 | config.program = ''; 352 | config.cancelReason = `Canceled the attachment because no running AutoHotkey script was found.`; 353 | return; 354 | } 355 | if (config.program === undefined) { 356 | const scriptPath = await vscode.window.showQuickPick(scriptPathList); 357 | if (scriptPath) { 358 | config.program = scriptPath; 359 | return; 360 | } 361 | config.program = ''; 362 | config.cancelReason = `Cancel the attach.`; 363 | return; 364 | } 365 | const isRunning = scriptPathList.map((scriptPath) => path.resolve(scriptPath.toLocaleLowerCase())).includes(path.resolve(config.program).toLowerCase()); 366 | if (!isRunning) { 367 | config.cancelReason = `Canceled the attach because "${String(config.program)}" is not running.`; 368 | } 369 | } 370 | if (!isString(config.program)) { 371 | throw Error('`program` must be a string.'); 372 | } 373 | if (config.request === 'launch' && !existsSync(config.program)) { 374 | throw Error(`\`program\` must be a file path that exists.\nSpecified: "${String(normalizePath(config.program))}"`); 375 | } 376 | if (config.program) { 377 | config.program = path.resolve(config.program); 378 | } 379 | })(); 380 | 381 | // init cwd 382 | ((): void => { 383 | if (!config.cwd) { 384 | config.cwd = path.dirname(config.program); 385 | } 386 | if (!isDirectory(config.cwd)) { 387 | throw Error(`\`cwd\` must be a absolute path of directory.\nSpecified: "${path.resolve(String(config.cwd))}"`); 388 | } 389 | config.cwd = path.resolve(config.cwd); 390 | })(); 391 | 392 | // init args 393 | ((): void => { 394 | if (!isArray(config.args)) { 395 | throw Error('`args` must be a array.'); 396 | } 397 | })(); 398 | 399 | // init env 400 | ((): void => { 401 | if (!isPlainObject(config.env)) { 402 | throw Error('`env` must be a object.'); 403 | } 404 | 405 | const env = {}; 406 | for (const [ key, value ] of Object.entries(process.env)) { 407 | env[key.toLowerCase()] = value; 408 | } 409 | for (const [ key, value ] of Object.entries(config.env)) { 410 | const a = value ?? ''; 411 | a; 412 | env[key.toLowerCase()] = value ?? ''; 413 | } 414 | 415 | config.env = env; 416 | })(); 417 | 418 | // init stopOnEntry 419 | ((): void => { 420 | if (!isBoolean(config.stopOnEntry)) { 421 | throw Error('`stopOnEntry` must be a boolean.'); 422 | } 423 | })(); 424 | 425 | // init maxChildren 426 | ((): void => { 427 | if (!Number.isInteger(config.maxChildren)) { 428 | throw Error('`maxChildren` must be a integer.'); 429 | } 430 | })(); 431 | 432 | // init openFileOnExit 433 | ((): void => { 434 | if (typeof config.openFileOnExit === 'undefined') { 435 | return; 436 | } 437 | 438 | if (!isString(config.openFileOnExit)) { 439 | throw Error('`openFileOnExit` must be a string.'); 440 | } 441 | if (!existsSync(config.openFileOnExit)) { 442 | throw Error(`\`openFileOnExit\` must be a file path that exists.\nSpecified: "${String(normalizePath(config.openFileOnExit))}"`); 443 | } 444 | })(); 445 | 446 | // init useIntelliSenseInDebugging 447 | ((): void => { 448 | if (!isBoolean(config.useIntelliSenseInDebugging)) { 449 | throw Error('`useIntelliSenseInDebugging` must be a boolean.'); 450 | } 451 | })(); 452 | 453 | // init usePerfTips 454 | ((): void => { 455 | if (!(isBoolean(config.usePerfTips) || isString(config.usePerfTips) || isPlainObject(config.usePerfTips))) { 456 | throw Error('`usePerfTips` must be a boolean, a string or a object.'); 457 | } 458 | 459 | const defaultUsePerfTips = { 460 | fontColor: 'gray', 461 | fontStyle: 'italic', 462 | format: '{{elapsedTime_s}}s elapsed', 463 | }; 464 | 465 | if (config.usePerfTips === true) { 466 | config.usePerfTips = defaultUsePerfTips; 467 | return; 468 | } 469 | if (typeof config.usePerfTips === 'string') { 470 | config.usePerfTips = { format: config.usePerfTips }; 471 | return; 472 | } 473 | defaults(config.usePerfTips, defaultUsePerfTips); 474 | })(); 475 | 476 | // init useDebugDirective 477 | ((): void => { 478 | if (!(isBoolean(config.useDebugDirective) || isPlainObject(config.useDebugDirective))) { 479 | throw Error('`useDebugDirective` must be a boolean or a object.'); 480 | } 481 | if (config.useDebugDirective === false) { 482 | return; 483 | } 484 | 485 | const defaultDirectiveComment = { 486 | useBreakpointDirective: true, 487 | useOutputDirective: true, 488 | useClearConsoleDirective: true, 489 | }; 490 | 491 | if (config.useDebugDirective === true) { 492 | config.useDebugDirective = defaultDirectiveComment; 493 | return; 494 | } 495 | defaults(config.useDebugDirective, defaultDirectiveComment); 496 | })(); 497 | 498 | // init useAutoJumpToError 499 | ((): void => { 500 | if (!isBoolean(config.useAutoJumpToError)) { 501 | throw Error('`useAutoJumpToError` must be a boolean.'); 502 | } 503 | })(); 504 | 505 | // init useOutputDebug 506 | ((): void => { 507 | if (!(isBoolean(config.useOutputDebug) || isPlainObject(config.useOutputDebug))) { 508 | throw Error('`useOutputDebug` must be a boolean or object.'); 509 | } 510 | const defaultUseOutputDebug = { 511 | category: 'stderr', 512 | useTrailingLinebreak: false, 513 | }; 514 | if (isPlainObject(config.useOutputDebug)) { 515 | defaults(config.useOutputDebug, defaultUseOutputDebug); 516 | return; 517 | } 518 | if (config.useOutputDebug) { 519 | config.useOutputDebug = defaultUseOutputDebug; 520 | } 521 | })(); 522 | 523 | 524 | // init skipFunctions 525 | ((): void => { 526 | if (!config.skipFunctions) { 527 | return; 528 | } 529 | if (!isArray(config.skipFunctions)) { 530 | throw Error('`skipFunctions` must be a array of string.'); 531 | } 532 | })(); 533 | 534 | // init skipFiles 535 | await (async(): Promise => { 536 | if (!config.skipFiles) { 537 | return; 538 | } 539 | if (!isArray(config.skipFiles)) { 540 | throw Error('`skipFiles` must be a array of string.'); 541 | } 542 | const skipFiles = config.skipFiles.map((filePath) => normalizeToUnix(String(filePath))); 543 | config.skipFiles = await glob(skipFiles, { onlyFiles: true, unique: true }); 544 | })(); 545 | 546 | // init useAnnounce 547 | ((): void => { 548 | if (!(isBoolean(config.useAnnounce) || [ 'error', 'detail' ].includes(config.useAnnounce))) { 549 | throw Error('`useAnnounce` must be a boolean, "error" or "detail".'); 550 | } 551 | })(); 552 | 553 | // init useLoadedScripts 554 | ((): void => { 555 | if (!(isBoolean(config.useLoadedScripts) || isPlainObject(config.useLoadedScripts))) { 556 | throw Error('`useLoadedScripts` must be a boolean or object.'); 557 | } 558 | 559 | const defaultValue = { 560 | scanImplicitLibrary: true, 561 | }; 562 | if (config.useLoadedScripts === true) { 563 | config.useLoadedScripts = defaultValue; 564 | } 565 | else { 566 | defaults(config.useLoadedScripts, defaultValue); 567 | } 568 | })(); 569 | 570 | // init variableCategories 571 | ((): void => { 572 | if (!config.variableCategories) { 573 | return; 574 | } 575 | if (config.variableCategories === 'recommend' || isArray(config.variableCategories)) { 576 | config.variableCategories = normalizeCategories(config.variableCategories as CategoriesData); 577 | return; 578 | } 579 | throw Error('`variableCategories` must be a "recommend" or array.'); 580 | })(); 581 | 582 | // init trace 583 | ((): void => { 584 | if (!isBoolean(config.trace)) { 585 | throw Error('`trace` must be a boolean.'); 586 | } 587 | })(); 588 | 589 | return config; 590 | } 591 | } 592 | class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { 593 | public createDebugAdapterDescriptor(_session: vscode.DebugSession): vscode.ProviderResult { 594 | return new vscode.DebugAdapterInlineImplementation(new AhkDebugSession()); 595 | } 596 | } 597 | 598 | export const activate = (context: vscode.ExtensionContext): void => { 599 | const provider = new AhkConfigurationProvider(); 600 | 601 | registerCommands(context); 602 | 603 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('ahk', provider)); 604 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('autohotkey', provider)); 605 | context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('autohotkey', new InlineDebugAdapterFactory())); 606 | context.subscriptions.push(vscode.languages.registerCompletionItemProvider([ 'ahk', 'ahk2', 'ah2', 'autohotkey', 'autohotkeynext', 'autohotkey2', 'autohotkeyl' ], completionItemProvider, '.')); 607 | 608 | const findWordRange = (ahkVersion: AhkVersion, document: vscode.TextDocument, position: vscode.Position, offset = 0): vscode.Range | undefined => { 609 | const range = document.getWordRangeAtPosition(position); 610 | if (!range) { 611 | return undefined; 612 | } 613 | const wordBeforeCursor = findWord(ahkVersion, document, position); 614 | const fixedRange = new vscode.Range(new vscode.Position(position.line, position.character - wordBeforeCursor.length), range.end); 615 | const debug = document.getText(fixedRange); debug; 616 | return fixedRange; 617 | }; 618 | context.subscriptions.push(vscode.languages.registerEvaluatableExpressionProvider([ 'ahk', 'autohotkeyl' ], { 619 | provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { 620 | const range = findWordRange(new AhkVersion('1.0'), document, position); 621 | if (!range) { 622 | return undefined; 623 | } 624 | return new vscode.EvaluatableExpression(range); 625 | }, 626 | })); 627 | context.subscriptions.push(vscode.languages.registerEvaluatableExpressionProvider([ 'ahk2', 'ah2', 'autohotkey', 'autohotkeynext', 'autohotkey2' ], { 628 | provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { 629 | const range = findWordRange(new AhkVersion('2.0'), document, position); 630 | if (!range) { 631 | return undefined; 632 | } 633 | return new vscode.EvaluatableExpression(range); 634 | }, 635 | })); 636 | }; 637 | -------------------------------------------------------------------------------- /src/util/AutoHotkeyLuncher.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync } from 'child_process'; 2 | import { EventEmitter } from 'events'; 3 | import * as path from 'path'; 4 | import { LaunchRequestArguments } from '../ahkDebug'; 5 | import { getAhkVersion } from './getAhkVersion'; 6 | import { escapePcreRegExEscape } from './stringUtils'; 7 | 8 | export type AutoHotkeyProcess = { 9 | command: string; 10 | event: EventEmitter; 11 | close: () => void; 12 | }; 13 | 14 | export class AutoHotkeyLauncher { 15 | private readonly launchRequest: LaunchRequestArguments; 16 | constructor(launchRequest: LaunchRequestArguments) { 17 | this.launchRequest = launchRequest; 18 | } 19 | public launch(): AutoHotkeyProcess { 20 | if (this.launchRequest.useUIAVersion) { 21 | return this.launchByCmd(); 22 | } 23 | return this.launchByNode(); 24 | } 25 | public launchByNode(): AutoHotkeyProcess { 26 | const { noDebug, runtime, cwd, hostname, port, runtimeArgs, program, args, env } = this.launchRequest; 27 | 28 | const launchArgs = [ 29 | ...(noDebug ? [] : [ `/Debug=${hostname}:${port}` ]), 30 | ...runtimeArgs, 31 | `${program}`, 32 | ...args, 33 | ]; 34 | const event = new EventEmitter(); 35 | const ahkProcess = spawn(runtime, launchArgs, { cwd, env }); 36 | ahkProcess.on('close', (exitCode?: number) => { 37 | event.emit('close', exitCode); 38 | }); 39 | ahkProcess.stdout.on('data', (data) => { 40 | event.emit('stdout', String(data)); 41 | }); 42 | ahkProcess.stderr.on('data', (data) => { 43 | event.emit('stderr', String(data)); 44 | }); 45 | 46 | return { 47 | command: `"${runtime}" ${launchArgs.join(' ')}`, 48 | event, 49 | close: (): void => { 50 | ahkProcess.kill(); 51 | }, 52 | }; 53 | } 54 | public launchByCmd(): AutoHotkeyProcess { 55 | const { noDebug, runtime, hostname, port, runtimeArgs, program, args, env } = this.launchRequest; 56 | const _runtimeArgs = runtimeArgs.filter((arg) => arg.toLowerCase() !== '/errorstdout'); 57 | const launchArgs = [ 58 | ...(noDebug ? [] : [ `/Debug=${hostname}:${port}` ]), 59 | ..._runtimeArgs, 60 | `"${program}"`, 61 | ...args, 62 | ]; 63 | 64 | const event = new EventEmitter(); 65 | const ahkProcess = spawn('cmd', [ '/c', '"', `"${runtime}"`, ...launchArgs, '"' ], { cwd: path.dirname(program), env, shell: true }); 66 | ahkProcess.on('close', (exitCode?: number) => { 67 | event.emit('close', exitCode); 68 | }); 69 | ahkProcess.stdout.on('data', (data) => { 70 | event.emit('stdout', String(data)); 71 | }); 72 | ahkProcess.stderr.on('data', (data) => { 73 | event.emit('stderr', String(data)); 74 | }); 75 | return { 76 | command: `"${runtime}" ${launchArgs.join(' ')}`, 77 | event, 78 | close: (): void => { 79 | ahkProcess.kill(); 80 | }, 81 | }; 82 | } 83 | public attach(): AutoHotkeyProcess | false { 84 | const { runtime, program, hostname, port } = this.launchRequest; 85 | 86 | const version = getAhkVersion(runtime); 87 | if (!version) { 88 | return false; 89 | } 90 | 91 | const ahkCode = version.mejor <= 1.1 ? ` 92 | DetectHiddenWindows On 93 | SetTitleMatchMode RegEx 94 | if (WinExist("i)${escapePcreRegExEscape(program)} ahk_class AutoHotkey")) { 95 | PostMessage DllCall("RegisterWindowMessage", "Str", "AHK_ATTACH_DEBUGGER"), DllCall("ws2_32\\inet_addr", "astr", "${hostname}"), ${port} 96 | ExitApp 97 | } 98 | ExitApp 1 99 | ` : ` 100 | A_DetectHiddenWindows := true 101 | SetTitleMatchMode("RegEx") 102 | if WinExist("i)${escapePcreRegExEscape(program)} ahk_class AutoHotkey") { 103 | PostMessage DllCall("RegisterWindowMessage", "Str", "AHK_ATTACH_DEBUGGER"), DllCall("ws2_32\\inet_addr", "astr", "${hostname}"), ${port} 104 | ExitApp 105 | } 106 | ExitApp(1) 107 | `; 108 | const result = spawnSync(runtime, [ '/ErrorStdOut', '*' ], { input: ahkCode }); 109 | if (result.error) { 110 | return false; 111 | } 112 | 113 | return { 114 | command: '', 115 | event: new EventEmitter(), 116 | close: (): void => {}, 117 | }; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/util/BreakpointManager.ts: -------------------------------------------------------------------------------- 1 | import * as dbgp from '../dbgpSession'; 2 | import { URI } from 'vscode-uri'; 3 | import { CaseInsensitiveMap } from './CaseInsensitiveMap'; 4 | import { equalsIgnoreCase } from './stringUtils'; 5 | 6 | export type BreakpointLogGroup = 'start' | 'startCollapsed' | 'end' | undefined; 7 | export interface BreakpointAdvancedData { 8 | condition?: string; 9 | hitCondition?: string; 10 | logMessage?: string; 11 | logGroup?: BreakpointLogGroup; 12 | hidden?: boolean; 13 | hitCount: number; 14 | unverifiedLine?: number; 15 | unverifiedColumn?: number; 16 | action?: () => Promise; 17 | } 18 | export type BreakpointKind = 'breakpoint' | 'logpoint' | 'conditional breakpoint' | 'conditional logpoint'; 19 | export class Breakpoint implements BreakpointAdvancedData { 20 | public id: number; 21 | public fileUri: string; 22 | public line: number; 23 | public condition: string; 24 | public hitCondition: string; 25 | public logMessage: string; 26 | public logGroup: BreakpointLogGroup; 27 | public hidden: boolean; 28 | public hitCount = 0; 29 | public unverifiedLine?: number; 30 | public unverifiedColumn?: number; 31 | public action?: () => Promise; 32 | public get filePath(): string { 33 | return URI.parse(this.fileUri).fsPath; 34 | } 35 | public get kind(): BreakpointKind { 36 | const logMode = Boolean(this.logMessage || this.logGroup); 37 | if (this.condition || this.hitCondition) { 38 | if (logMode) { 39 | return 'conditional logpoint'; 40 | } 41 | return 'conditional breakpoint'; 42 | } 43 | return logMode ? 'logpoint' : 'breakpoint'; 44 | } 45 | constructor(dbgpBreakpoint: dbgp.Breakpoint, advancedData?: BreakpointAdvancedData) { 46 | this.id = dbgpBreakpoint.id; 47 | this.fileUri = dbgpBreakpoint.fileUri; 48 | this.line = dbgpBreakpoint.line; 49 | 50 | this.condition = advancedData?.condition ?? ''; 51 | this.hitCondition = advancedData?.hitCondition ?? ''; 52 | this.logMessage = advancedData?.logMessage ?? ''; 53 | this.logGroup = advancedData?.logGroup; 54 | this.hidden = advancedData?.hidden ?? false; 55 | this.unverifiedLine = advancedData?.unverifiedLine ?? dbgpBreakpoint.line; 56 | this.unverifiedColumn = advancedData?.unverifiedColumn; 57 | this.action = advancedData?.action; 58 | } 59 | } 60 | export class LineBreakpoints extends Array { 61 | public get fileUri(): string { 62 | return 0 < this.length ? this[0].fileUri : ''; 63 | } 64 | public get filePath(): string { 65 | return 0 < this.length ? this[0].filePath : ''; 66 | } 67 | public get line(): number { 68 | return 0 < this.length ? this[0].line : -1; 69 | } 70 | public incrementHitCount(): void { 71 | this.forEach((breakpoint) => { 72 | breakpoint.hitCount++; 73 | }); 74 | } 75 | public decrementHitCount(): void { 76 | this.forEach((breakpoint) => { 77 | breakpoint.hitCount--; 78 | }); 79 | } 80 | public hasAdvancedBreakpoint(): boolean { 81 | for (const breakpoint of this) { 82 | const { condition, hitCondition, logMessage, logGroup, action } = breakpoint; 83 | if (condition || hitCondition || logMessage || logGroup || action) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | } 89 | } 90 | 91 | export class BreakpointManager { 92 | private readonly session: dbgp.Session; 93 | private readonly breakpointsMap = new CaseInsensitiveMap(); 94 | constructor(session: dbgp.Session) { 95 | this.session = session; 96 | } 97 | public hasBreakpoint(fileUri: string, line: number): boolean { 98 | const key = this.createKey(fileUri, line); 99 | return this.breakpointsMap.has(key); 100 | } 101 | public getLineBreakpoints(fileUri: string, line: number): LineBreakpoints | null { 102 | const key = this.createKey(fileUri, line); 103 | for (const [ targetKey, lineBreakpoints ] of this.breakpointsMap) { 104 | if (key === targetKey) { 105 | return lineBreakpoints; 106 | } 107 | } 108 | return null; 109 | } 110 | public async registerBreakpoint(fileUriOrBreakpoint: string | Breakpoint, line: number, advancedData?: BreakpointAdvancedData): Promise { 111 | let fileUri: string, unverifiedLine: number, _advancedData: BreakpointAdvancedData | undefined; 112 | if (fileUriOrBreakpoint instanceof Breakpoint) { 113 | const breakpoint = fileUriOrBreakpoint; 114 | if (breakpoint.hidden) { 115 | return fileUriOrBreakpoint; 116 | } 117 | fileUri = breakpoint.fileUri; 118 | unverifiedLine = breakpoint.line; 119 | _advancedData = breakpoint as BreakpointAdvancedData; 120 | } 121 | else { 122 | fileUri = fileUriOrBreakpoint; 123 | if (!line) { 124 | throw new TypeError('The second argument is not specified.'); 125 | } 126 | unverifiedLine = line; 127 | _advancedData = advancedData; 128 | } 129 | 130 | const response = await this.session.sendBreakpointSetCommand(fileUri, unverifiedLine); 131 | const settedBreakpoint = new Breakpoint((await this.session.sendBreakpointGetCommand(response.id)).breakpoint, _advancedData); 132 | const verifiedLine = settedBreakpoint.line; 133 | 134 | let registeredLineBreakpoints: LineBreakpoints; 135 | if (this.hasBreakpoint(fileUri, verifiedLine)) { 136 | registeredLineBreakpoints = this.getLineBreakpoints(fileUri, verifiedLine)!; 137 | registeredLineBreakpoints.push(settedBreakpoint); 138 | registeredLineBreakpoints.sort((a, b) => { 139 | if (a.hidden !== b.hidden) { 140 | return Number(a.hidden) - Number(b.hidden); 141 | } 142 | if (a.unverifiedColumn && b.unverifiedColumn) { 143 | return a.unverifiedColumn - b.unverifiedColumn; 144 | } 145 | const lineA = a.unverifiedLine ?? a.line; 146 | const lineB = b.unverifiedLine ?? b.line; 147 | return lineA - lineB; 148 | }); 149 | } 150 | else { 151 | registeredLineBreakpoints = new LineBreakpoints(settedBreakpoint); 152 | const key = this.createKey(fileUri, verifiedLine); 153 | this.breakpointsMap.set(key, registeredLineBreakpoints); 154 | } 155 | return settedBreakpoint; 156 | } 157 | public async unregisterLineBreakpoints(fileUri: string, line: number): Promise { 158 | const breakpoints = this.getLineBreakpoints(fileUri, line); 159 | if (!breakpoints || breakpoints.length === 0) { 160 | return; 161 | } 162 | 163 | const id = breakpoints[0].id; 164 | const key = this.createKey(fileUri, line); 165 | const hiddenBreakpoints = breakpoints.filter((breakpoint) => breakpoint.hidden); 166 | if (hiddenBreakpoints.length === 0 && this.breakpointsMap.has(key)) { 167 | try { 168 | await this.session.sendBreakpointRemoveCommand(id); 169 | this.breakpointsMap.delete(key); 170 | } 171 | catch { 172 | } 173 | } 174 | else { 175 | this.breakpointsMap.set(key, new LineBreakpoints(...hiddenBreakpoints)); 176 | } 177 | } 178 | public async unregisterBreakpointsInFile(fileUri: string): Promise { 179 | const targetFilePath = URI.parse(fileUri).fsPath; 180 | 181 | const removedBreakpoints: Breakpoint[] = []; 182 | for await (const [ , lineBreakpoints ] of this.breakpointsMap) { 183 | if (equalsIgnoreCase(targetFilePath, lineBreakpoints.filePath)) { 184 | await this.unregisterLineBreakpoints(fileUri, lineBreakpoints.line); 185 | removedBreakpoints.push(...lineBreakpoints.filter((breakpoint) => !breakpoint.hidden)); 186 | } 187 | } 188 | return removedBreakpoints; 189 | } 190 | private createKey(fileUri: string, line: number): string { 191 | // The following encoding differences have been converted to path 192 | // file:///W%3A/project/vscode-autohotkey-debug/demo/demo.ahk" 193 | // file:///w:/project/vscode-autohotkey-debug/demo/demo.ahk 194 | 195 | const uri = URI.parse(fileUri); 196 | const filePath = uri.scheme === 'file' ? uri.fsPath.toLowerCase() : fileUri.toLowerCase(); 197 | return `${filePath},${line}`; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/util/CaseInsensitiveMap.ts: -------------------------------------------------------------------------------- 1 | export class CaseInsensitiveMap extends Map { 2 | public get(key: K): V | undefined { 3 | const _key = typeof key === 'string' ? key.toLowerCase() : key; 4 | return super.get(_key as K); 5 | } 6 | public set(key: K, value: V): this { 7 | const _key = typeof key === 'string' ? key.toLowerCase() : key; 8 | return super.set(_key as K, value); 9 | } 10 | public has(key: K): boolean { 11 | const _key = typeof key === 'string' ? key.toLowerCase() : key; 12 | return super.has(_key as K); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/util/ConditionEvaluator.ts: -------------------------------------------------------------------------------- 1 | import createPcre from 'pcre-to-regexp'; 2 | import regexParser from 'regex-parser'; 3 | import { Parser, createParser } from './ConditionParser'; 4 | import * as dbgp from '../dbgpSession'; 5 | import { LazyMetaVariableValue, MetaVariableValue, MetaVariableValueMap } from './VariableManager'; 6 | import { isFloatLike, isIntegerLike, isNumberLike, isPrimitive } from './util'; 7 | import { isObject } from 'ts-predicates'; 8 | 9 | type Value = string | number | boolean | { address: number } | undefined; 10 | type Operator = (a: Value, b: Value) => boolean; 11 | const not = (predicate): Operator => (a, b): boolean => !predicate(a, b); 12 | const equals: Operator = (a, b) => { 13 | const _a = isObject(a) ? a.address : a; 14 | const _b = isObject(b) ? b.address : b; 15 | // eslint-disable-next-line eqeqeq 16 | return _a == _b; 17 | }; 18 | const equalsIgnoreCase: Operator = (a, b) => { 19 | if (isObject(a) || isObject(b)) { 20 | return equals(a, b); 21 | } 22 | return String(a).toLowerCase() === String(b).toLocaleLowerCase(); 23 | }; 24 | const inequality = (sign: string): Operator => { 25 | return (a, b): boolean => { 26 | if (typeof a === 'undefined' || typeof b === 'undefined') { 27 | return false; 28 | } 29 | if (isObject(a) || isObject(b)) { 30 | return false; 31 | } 32 | 33 | const _a = typeof a === 'string' ? parseFloat(a) : a; 34 | const _b = typeof b === 'string' ? parseFloat(b) : b; 35 | if (Number.isNaN(_a) || Number.isNaN(_b)) { 36 | return false; 37 | } 38 | 39 | if (sign === '<') { 40 | return _a < _b; 41 | } 42 | if (sign === '<=') { 43 | return _a <= _b; 44 | } 45 | if (sign === '>') { 46 | return _a > _b; 47 | } 48 | if (sign === '>=') { 49 | return _a >= _b; 50 | } 51 | return false; 52 | }; 53 | }; 54 | const ahkRegexToJsRegex = (ahkRegex: string): RegExp => { 55 | if (ahkRegex.startsWith('/')) { 56 | return regexParser(ahkRegex); 57 | } 58 | 59 | const match = ahkRegex.match(/(?.+)\)(?.+)/ui); 60 | let flags: string, pattern: string; 61 | if (match?.groups) { 62 | flags = match.groups.flags; 63 | pattern = match.groups.pattern; 64 | } 65 | else { 66 | flags = ''; 67 | pattern = ahkRegex; 68 | } 69 | 70 | return createPcre(`%${pattern}%${flags}`); 71 | }; 72 | const regexCompare: Operator = function(input, ahkRegex) { 73 | if (typeof input === 'undefined' || typeof ahkRegex === 'undefined') { 74 | return false; 75 | } 76 | if (isObject(input) || isObject(ahkRegex)) { 77 | return false; 78 | } 79 | 80 | const regex = ahkRegexToJsRegex(String(ahkRegex)); 81 | return regex.test(String(input)); 82 | }; 83 | const comparisonOperators: { [key: string]: Operator} = { 84 | '=': equalsIgnoreCase, 85 | '==': equals, 86 | '!=': not(equalsIgnoreCase), 87 | '!==': not(equals), 88 | '~=': regexCompare, 89 | '!~': not(regexCompare), 90 | '<': inequality('<'), 91 | '<=': inequality('<='), 92 | '>': inequality('>'), 93 | '>=': inequality('>='), 94 | }; 95 | const logicalOperators: { [key: string]: Operator} = { 96 | '&&': (a, b) => Boolean(a && b), 97 | '||': (a, b) => Boolean(a ?? b), 98 | }; 99 | 100 | export class ConditionalEvaluator { 101 | private readonly session: dbgp.Session; 102 | private readonly parser: Parser; 103 | constructor(session: dbgp.Session) { 104 | this.session = session; 105 | this.parser = createParser(this.session.ahkVersion); 106 | } 107 | public async eval(expressions: string, metaVariableMap: MetaVariableValueMap): Promise { 108 | const parsed = this.parser.Expressions.parse(expressions.trim()); 109 | if (!parsed.status) { 110 | return false; 111 | } 112 | 113 | const exprs = parsed.value.value; 114 | if (Array.isArray(exprs)) { 115 | const [ left, operatorName, right, rest ] = exprs; 116 | const a = await this.evalExpression(left.value, metaVariableMap); 117 | const operator = logicalOperators[operatorName]; 118 | const b = await this.evalExpression(right.value, metaVariableMap); 119 | const result = operator(a, b); 120 | if (rest) { 121 | return this.eval(`${String(result)}${String(rest)}`, metaVariableMap); 122 | } 123 | return result; 124 | } 125 | 126 | return this.evalExpression(exprs.value, metaVariableMap); 127 | } 128 | public async evalExpression(expression: { type: string; value: any}, metaVariableMap: MetaVariableValueMap): Promise { 129 | let primitiveValue; 130 | if (expression.type === 'BinaryExpression') { 131 | const [ a, operatorType, b ] = expression.value; 132 | 133 | if (operatorType.type === 'ComparisonOperator') { 134 | const operator = comparisonOperators[operatorType.value]; 135 | const getValue = async(parsed): Promise => { 136 | const value = await this.evalValue(parsed, metaVariableMap); 137 | 138 | if (this.session.ahkVersion.mejor <= 1.1 && !value) { 139 | return ''; 140 | } 141 | 142 | if (isPrimitive(value)) { 143 | return value; 144 | } 145 | else if (value instanceof dbgp.ObjectProperty) { 146 | return { address: value.address }; 147 | } 148 | else if (value instanceof dbgp.PrimitiveProperty) { 149 | return value.value; 150 | } 151 | 152 | return undefined; 153 | }; 154 | const _a = await getValue(a); 155 | const _b = await getValue(b); 156 | 157 | if (typeof _a !== 'undefined' && typeof _b !== 'undefined') { 158 | return operator(_a, _b); 159 | } 160 | } 161 | else if ([ 'IsOperator', 'InOperator', 'HasOperator', 'ContainsOperator' ].includes(operatorType.type)) { 162 | const negativeMode = -1 < operatorType.value.search(/not/ui); 163 | const valueA = await this.evalValue(a, metaVariableMap); 164 | const valueB = await this.evalValue(b, metaVariableMap); 165 | 166 | let result = false; 167 | if (operatorType.type === 'IsOperator') { 168 | if (valueA instanceof dbgp.ObjectProperty && valueB instanceof dbgp.ObjectProperty) { 169 | let baseName = `${valueA.fullName}.`; 170 | let baseClassNameProperty = await this.session.evaluate(`${baseName}.__class`); 171 | while (baseClassNameProperty) { 172 | // eslint-disable-next-line no-await-in-loop 173 | baseClassNameProperty = await this.session.evaluate(`${baseName}.__class`); 174 | if (baseClassNameProperty instanceof dbgp.PrimitiveProperty) { 175 | if (valueB.fullName === baseClassNameProperty.value) { 176 | result = true; 177 | break; 178 | } 179 | } 180 | 181 | baseName += '.'; 182 | } 183 | } 184 | else if ((valueA instanceof dbgp.Property || isPrimitive(valueA)) && (valueB instanceof dbgp.PrimitiveProperty || typeof valueB === 'string')) { 185 | const valueAType = valueA instanceof dbgp.Property ? valueA.type : String(a.value?.value?.type ?? a.value.type ?? 'string').toLowerCase(); 186 | const valueBType = String(valueB instanceof dbgp.PrimitiveProperty ? valueB.value : valueB) 187 | .toLowerCase() 188 | .replace(/(?<=\b)int(?=\b)/ui, 'integer'); 189 | 190 | if (valueAType === valueBType) { 191 | result = true; 192 | } 193 | else if (valueBType === 'primitive') { 194 | if ([ 'string', 'integer', 'float' ].includes(valueAType)) { 195 | result = true; 196 | } 197 | } 198 | else if (valueBType === 'number') { 199 | if ([ 'integer', 'float' ].includes(valueAType)) { 200 | result = true; 201 | } 202 | } 203 | else if (valueA instanceof dbgp.ObjectProperty && valueBType.startsWith('object:')) { 204 | const className = valueBType.match(/^object:(.*)$/ui)![1]; 205 | if (className.toLowerCase() === valueA.className.toLowerCase()) { 206 | result = true; 207 | } 208 | } 209 | else if (valueA instanceof dbgp.PrimitiveProperty || isPrimitive(valueA)) { 210 | const _valueA = String(valueA instanceof dbgp.PrimitiveProperty ? valueA.value : valueA); 211 | if (valueBType === 'integer:like' && isIntegerLike(_valueA)) { 212 | result = true; 213 | } 214 | else if (valueBType === 'float:like' && isFloatLike(_valueA)) { 215 | result = true; 216 | } 217 | else if (valueBType === 'number:like' && isNumberLike(_valueA)) { 218 | result = true; 219 | } 220 | else if (valueBType === 'string:alpha' && -1 < _valueA.search(/^[a-zA-Z]+$/u)) { 221 | result = true; 222 | } 223 | else if (valueBType === 'string:alnum' && -1 < _valueA.search(/^[a-zA-Z0-9]+$/u)) { 224 | result = true; 225 | } 226 | else if (valueBType === 'string:upper' && -1 < _valueA.search(/^[A-Z]+$/u)) { 227 | result = true; 228 | } 229 | else if (valueBType === 'string:lower' && -1 < _valueA.search(/^[a-z]+$/u)) { 230 | result = true; 231 | } 232 | else if (valueBType === 'string:space' && -1 < _valueA.search(/^\s+$/u)) { 233 | result = true; 234 | } 235 | else if (valueBType === 'string:hex' && -1 < _valueA.search(/^0x[0-9a-fA-F]+$/u)) { 236 | result = true; 237 | } 238 | else if (valueBType === 'string:time' && !Number.isNaN(Date.parse(_valueA))) { 239 | result = true; 240 | } 241 | } 242 | } 243 | else if (!valueA && valueB === 'undefined') { 244 | result = true; 245 | } 246 | } 247 | else if (operatorType.type === 'InOperator' && valueB instanceof dbgp.ObjectProperty) { 248 | if (valueA instanceof dbgp.PrimitiveProperty || typeof valueA === 'string') { 249 | const keyName = valueA instanceof dbgp.PrimitiveProperty ? valueA.value : valueA; 250 | const property = await this.session.evaluate(`${valueB.fullName}.${keyName}`); 251 | if (property) { 252 | result = true; 253 | } 254 | } 255 | } 256 | else if (operatorType.type === 'HasOperator' && (valueA instanceof dbgp.ObjectProperty || valueA instanceof Object)) { 257 | const keys = valueA instanceof dbgp.ObjectProperty ? valueA.children.map((child) => (child.isIndexKey ? String(child.index) : child.name)) : Object.keys(valueA); 258 | if (valueB instanceof dbgp.ObjectProperty) { 259 | result = keys.some((key) => { 260 | const match = key.match(/^\[Object\((?
\d+)\)\]$/u); 261 | return match?.groups?.address === String(valueB.address); 262 | }); 263 | } 264 | else { 265 | const searchValue = valueB instanceof dbgp.PrimitiveProperty ? valueB.value : valueB; 266 | const isRegExp = String(searchValue).startsWith('/'); 267 | result = isPrimitive(searchValue) ? keys.some((key) => { 268 | if (key === '') { 269 | return false; 270 | } 271 | const fixedName = key.replace(/^\["|^<|"\]$|>$/gu, ''); 272 | return isRegExp ? regexCompare(fixedName, searchValue) : equals(fixedName, searchValue); 273 | }) : false; 274 | } 275 | } 276 | else if (operatorType.type === 'ContainsOperator') { 277 | const children = valueA instanceof dbgp.ObjectProperty 278 | ? valueA.children.map((child) => (child instanceof dbgp.PrimitiveProperty ? { type: 'primitive', value: child.value } : { type: 'object', value: (child as dbgp.ObjectProperty).address })) 279 | : Object.entries(valueA ?? {}).map(([ key, value ]) => (isPrimitive(value) ? { type: 'primitive', value } : { type: 'object' })); 280 | if (valueB instanceof dbgp.Property || isPrimitive(valueB)) { 281 | if (valueB instanceof dbgp.ObjectProperty) { 282 | result = children.some((child) => { 283 | if (child.type === 'object') { 284 | return child.value === valueB.address; 285 | } 286 | return false; 287 | }); 288 | } 289 | else { 290 | const searchValue = valueB instanceof dbgp.PrimitiveProperty ? valueB.value : valueB; 291 | const isRegExp = String(searchValue).startsWith('/'); 292 | result = isPrimitive(searchValue) ? children.some((child) => { 293 | if (child.type === 'object') { 294 | return false; 295 | } 296 | return isRegExp ? regexCompare(child.value, searchValue) : equals(child.value, searchValue); 297 | }) : false; 298 | } 299 | } 300 | } 301 | 302 | if (negativeMode) { 303 | return !result; 304 | } 305 | return result; 306 | } 307 | } 308 | else if (expression.type === 'MetaVariable') { 309 | if (metaVariableMap.has(expression.value)) { 310 | primitiveValue = metaVariableMap.get(expression.value)!; 311 | } 312 | } 313 | else if (expression.type === 'PropertyName') { 314 | const property = await this.evalProperty(expression); 315 | if (property instanceof dbgp.PrimitiveProperty) { 316 | primitiveValue = property.value; 317 | } 318 | else if (property instanceof dbgp.ObjectProperty) { 319 | primitiveValue = String(property.address); 320 | } 321 | else if (typeof property === 'string') { 322 | primitiveValue = property; 323 | } 324 | } 325 | else if (expression.type === 'Primitive') { 326 | const primitive = expression.value; 327 | if (primitive.type === 'String') { 328 | const string = expression.value; 329 | primitiveValue = string.value; 330 | } 331 | else if (primitive.type === 'Number') { 332 | const number = expression.value; 333 | primitiveValue = number.value.value; 334 | } 335 | else { 336 | const boolean = primitive; 337 | primitiveValue = boolean.value; 338 | } 339 | } 340 | 341 | if (typeof primitiveValue === 'string') { 342 | if (isNumberLike(primitiveValue)) { 343 | const number = Number(primitiveValue); 344 | return Boolean(number); 345 | } 346 | return primitiveValue !== ''; 347 | } 348 | return false; 349 | } 350 | private async evalProperty(parsed): Promise { 351 | if (!('type' in parsed || 'value' in parsed)) { 352 | return undefined; 353 | } 354 | 355 | const propertyName = parsed.value; 356 | if (parsed?.extraInfo === 'countof') { 357 | const property = await this.session.evaluate(propertyName); 358 | if (property instanceof dbgp.ObjectProperty) { 359 | const maxIndex = property.maxIndex; 360 | if (maxIndex) { 361 | return String(maxIndex); 362 | } 363 | const children = property.children.filter((element) => element.name !== ''); 364 | return String(children.length); 365 | } 366 | else if (property instanceof dbgp.PrimitiveProperty) { 367 | return String(property.value.length); 368 | } 369 | return undefined; 370 | } 371 | return this.session.evaluate(propertyName); 372 | } 373 | private async evalValue(parsed, metaVariableMap: MetaVariableValueMap): Promise { 374 | if (!('type' in parsed || 'value' in parsed)) { 375 | return undefined; 376 | } 377 | 378 | if (parsed.type === 'MetaVariable') { 379 | const metaVariable = metaVariableMap.get(parsed.value); 380 | return metaVariable; 381 | } 382 | else if (parsed.type === 'PropertyName') { 383 | return this.evalProperty(parsed); 384 | } 385 | else if (parsed.type === 'Primitive') { 386 | const primitive = parsed.value; 387 | if (primitive.type === 'String') { 388 | if (parsed?.extraInfo === 'countof') { 389 | const value = String(primitive.value); 390 | return String(value.length); 391 | } 392 | return String(primitive.value); 393 | } 394 | if (primitive.type === 'Boolean') { 395 | return String(primitive.value); 396 | } 397 | 398 | const number = primitive.value; 399 | if (parsed?.extraInfo === 'countof') { 400 | const value = String(number.value); 401 | return String(value.length); 402 | } 403 | if (typeof number.value === 'number') { 404 | return number.value as number; 405 | } 406 | return String(number.value); 407 | } 408 | else if (parsed.type === 'RegExp') { 409 | return String(parsed.value); 410 | } 411 | 412 | return undefined; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/util/ConditionParser.ts: -------------------------------------------------------------------------------- 1 | import * as P from 'parsimmon'; 2 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 3 | 4 | export type Parser = P.Language; 5 | export const createParser = function(version: AhkVersion): P.Language { 6 | return P.createLanguage({ 7 | _(rules) { 8 | return P.regex(/\s*/u); 9 | }, 10 | __(rules) { 11 | return P.whitespace; 12 | }, 13 | StringLiteral(rules) { 14 | return version.mejor <= 1.1 15 | ? rules.StringDoubleLiteral 16 | : P.alt(rules.StringDoubleLiteral, rules.StringSingleLiteral); 17 | }, 18 | StringDoubleLiteral(rules) { 19 | return P.seq( 20 | P.string('"'), 21 | version.mejor <= 1.1 22 | ? P.regex(/(?:``|""|[^"\n])*/ui) 23 | : P.regex(/(?:``|`"|[^"\n])*/ui), 24 | P.string('"'), 25 | ).map((result) => { 26 | const convertedEscape = result[1] 27 | .replace(version.mejor <= 1.1 ? /""/gu : /`"/gu, '"') 28 | .replace(version.mejor <= 1.1 ? /`(,|%|`|;|:)/gu : /`(`|;|:|\{)/gu, '$1') 29 | .replace(/`n/gu, '\n') 30 | .replace(/`r/gu, '\r') 31 | .replace(/`b/gu, '\b') 32 | .replace(/`t/gu, '\t') 33 | .replace(/`v/gu, '\v') 34 | .replace(/`a/gu, '\x07') 35 | .replace(/`f/gu, '\f'); 36 | return { 37 | type: 'String', 38 | value: convertedEscape, 39 | }; 40 | }); 41 | }, 42 | StringSingleLiteral(rules) { 43 | return P.seq( 44 | P.string(`'`), 45 | P.regex(/(?:``|`'|[^'\n])*/ui), 46 | P.string(`'`), 47 | ).map((result) => { 48 | const convertedEscape = result[1] 49 | .replace(/`'/gu, '\'') 50 | .replace(/`(`|;|:|\{)/gu, '$1') 51 | .replace(/`n/gu, '\n') 52 | .replace(/`r/gu, '\r') 53 | .replace(/`b/gu, '\b') 54 | .replace(/`t/gu, '\t') 55 | .replace(/`v/gu, '\v') 56 | .replace(/`a/gu, '\x07') 57 | .replace(/`f/gu, '\f'); 58 | return { 59 | type: 'String', 60 | value: convertedEscape, 61 | }; 62 | }); 63 | }, 64 | NumberLiteral(rules) { 65 | return P.alt( 66 | rules.ScientificLiteral, 67 | rules.HexLiteral, 68 | rules.FloatLiteral, 69 | rules.IntegerLiteral, 70 | ).map((result) => { 71 | return { 72 | type: 'Number', 73 | value: result, 74 | }; 75 | }); 76 | }, 77 | NegativeOperator() { 78 | return P.string('-'); 79 | }, 80 | IntegerLiteral(rules) { 81 | return P.regex(/(-)?(?:[1-9][0-9]+|[0-9])/ui).map((result) => { 82 | return { 83 | type: 'Integer', 84 | value: parseInt(result, 10), 85 | }; 86 | }); 87 | }, 88 | FloatLiteral(rules) { 89 | return P.seq( 90 | rules.IntegerLiteral, 91 | P.regex(/\.[0-9]+/ui), 92 | ).map((result) => { 93 | const floatString = `${String(result[0].value)}${result[1]}`; 94 | return { 95 | type: 'Float', 96 | value: 2 <= version.mejor ? parseFloat(floatString) : floatString, 97 | }; 98 | }); 99 | }, 100 | HexLiteral(rules) { 101 | return P.regex(/(-)?0x(?:[0-9a-f]+)/ui).map((result) => { 102 | const hexString = result; 103 | return { 104 | type: 'Hex', 105 | value: 2 <= version.mejor ? parseInt(hexString, 16) : hexString, 106 | }; 107 | }); 108 | }, 109 | ScientificLiteral(rules) { 110 | return P.seq( 111 | version.mejor <= 1.1 112 | ? rules.FloatLiteral 113 | : P.alt(rules.FloatLiteral, rules.IntegerLiteral), 114 | P.regex(/e[+]?\d+/ui), 115 | ).map((result) => { 116 | const rawValue = `${String(result[0].value)}${result[1]}`; 117 | return { 118 | type: 'Scientific', 119 | value: version.mejor <= 1.1 ? rawValue : Number(rawValue).toFixed(1), 120 | }; 121 | }); 122 | }, 123 | BooleanLiteral(rules) { 124 | return P.regex(/true|false/ui).map((result) => { 125 | return { 126 | type: 'Boolean', 127 | value: (/^true$/ui).test(result) ? '1' : '0', 128 | }; 129 | }); 130 | }, 131 | RegexpLiteral(rules) { 132 | return P.seq( 133 | P.string('/'), 134 | P.regex(/(\/|[^/])+(?=\/)/u), 135 | P.alt( 136 | P.seq( 137 | P.string('/'), 138 | P.regex(/([gimsuy]+)(?=\b)/u), 139 | ).map((result) => result.join('')), 140 | P.string('/'), 141 | ), 142 | ).map((result) => { 143 | return { 144 | type: 'RegExp', 145 | value: result.join(''), 146 | }; 147 | }); 148 | }, 149 | Identifer(rules) { 150 | return version.mejor <= 1.1 151 | ? P.regex(/[\w#@$]+/ui) 152 | : P.regex(/[\w]+/ui); 153 | }, 154 | PropertyAccesor(rules) { 155 | return P.seq( 156 | P.string('.'), 157 | rules.Identifer, 158 | ).map((result) => result.join('')); 159 | }, 160 | IndexAccesor(rules) { 161 | return P.seq( 162 | P.string('['), 163 | P.alt(rules.Primitive, rules.PropertyName), 164 | P.string(']'), 165 | ).map((result) => { 166 | if ('type' in result[1] && result[1].type === 'Primitive') { 167 | const primitive = result[1].value; 168 | if (primitive.type === 'String') { 169 | return `["${String(result[1].value.value)}"]`; 170 | } 171 | return `[${String(result[1].value.value.value)}]`; 172 | } 173 | return `[${String(result[1].value)}]`; 174 | }); 175 | }, 176 | BaseAccesor(rules) { 177 | return P.string('.'); 178 | }, 179 | PropertyName(rules) { 180 | return P.seq( 181 | rules.Identifer, 182 | P.alt( 183 | rules.PropertyAccesor, 184 | rules.IndexAccesor, 185 | rules.BaseAccesor, 186 | ).many(), 187 | ).map((result) => { 188 | return { 189 | type: 'PropertyName', 190 | value: `${String(result[0])}${result[1].join('')}`, 191 | }; 192 | }); 193 | }, 194 | MetaVariable(rules) { 195 | return P.seq( 196 | P.string('{'), 197 | rules.PropertyName, 198 | P.string('}'), 199 | ).map((result) => { 200 | return { 201 | type: 'MetaVariable', 202 | value: result[1].value, 203 | }; 204 | }); 205 | }, 206 | Operand(rules) { 207 | return P.alt( 208 | P.seq( 209 | P.alt(rules.CountofOperator), 210 | P.alt( 211 | rules.Primitive, 212 | rules.RegexpLiteral, 213 | rules.PropertyName, 214 | rules.MetaVariable, 215 | ), 216 | ).map((result) => { 217 | return { 218 | type: result[1].type, 219 | value: result[1].value, 220 | extraInfo: result[0].value, 221 | }; 222 | }), 223 | P.alt( 224 | rules.Primitive, 225 | rules.RegexpLiteral, 226 | rules.PropertyName, 227 | rules.MetaVariable, 228 | ), 229 | ); 230 | }, 231 | Primitive(rules) { 232 | return P.alt( 233 | rules.StringLiteral, 234 | rules.NumberLiteral, 235 | rules.BooleanLiteral, 236 | ).map((result) => { 237 | return { 238 | type: 'Primitive', 239 | value: result, 240 | }; 241 | }); 242 | }, 243 | Expressions(rules) { 244 | return P.alt( 245 | P.seq( 246 | rules.Expression, 247 | rules._, 248 | rules.LogicalOperator, 249 | rules._, 250 | rules.Expression, 251 | P.regex(/.*/u), // rest expression 252 | ).node(''), 253 | rules.Expression.node(''), 254 | ).map((result) => { 255 | const pos = { 256 | start: result.start, 257 | end: result.end, 258 | }; 259 | 260 | if (result.value.length === 6) { 261 | return { 262 | type: 'Expressions', 263 | value: [ result.value[0], result.value[2], result.value[4], result.value[5] ], 264 | pos, 265 | }; 266 | } 267 | return { 268 | type: 'Expression', 269 | value: result.value, 270 | pos, 271 | }; 272 | }); 273 | }, 274 | Expression(rules) { 275 | return P.alt( 276 | rules.BinaryExpression, 277 | rules.Operand, 278 | ).map((result) => { 279 | return { 280 | type: 'Expression', 281 | value: result, 282 | }; 283 | }); 284 | }, 285 | BinaryExpression(rules) { 286 | return P.seq( 287 | rules.Operand, 288 | rules.Operator, 289 | rules.Operand, 290 | ).map((result) => { 291 | return { 292 | type: 'BinaryExpression', 293 | value: result, 294 | }; 295 | }); 296 | }, 297 | Operator(rules) { 298 | return P.alt( 299 | rules.ComparisonOperator, 300 | rules.IsOperator, 301 | rules.InOperator, 302 | rules.HasOperator, 303 | rules.ContainsOperator, 304 | ); 305 | }, 306 | LogicalOperator(rules) { 307 | return P.alt(P.string('&&'), P.string('||')); 308 | }, 309 | ComparisonOperator(rules) { 310 | return P.seq( 311 | rules._, 312 | P.alt( 313 | P.string('=='), 314 | P.string('='), 315 | P.string('!=='), 316 | P.string('!='), 317 | P.string('<='), 318 | P.string('<'), 319 | P.string('>='), 320 | P.string('>'), 321 | P.string('~='), 322 | P.string('!~'), 323 | ), 324 | rules._, 325 | ).map((result) => { 326 | return { 327 | type: 'ComparisonOperator', 328 | value: result[1], 329 | }; 330 | }); 331 | }, 332 | IsOperator(rules) { 333 | return P.regex(/\s+is(\s+not)?\s+/ui).map((result) => { 334 | return { 335 | type: 'IsOperator', 336 | value: result.toLowerCase().trim(), 337 | }; 338 | }); 339 | }, 340 | InOperator(rules) { 341 | return P.regex(/\s+(not\s+)?in\s+/ui).map((result) => { 342 | return { 343 | type: 'InOperator', 344 | value: result.toLowerCase().trim(), 345 | }; 346 | }); 347 | }, 348 | HasOperator(rules) { 349 | return P.regex(/\s+has(\s+not)?\s+/ui).map((result) => { 350 | return { 351 | type: 'HasOperator', 352 | value: result.toLowerCase().trim(), 353 | }; 354 | }); 355 | }, 356 | ContainsOperator(rules) { 357 | return P.regex(/\s+(not\s+)?contains\s+/ui).map((result) => { 358 | return { 359 | type: 'ContainsOperator', 360 | value: result.toLowerCase().trim(), 361 | }; 362 | }); 363 | }, 364 | CountofOperator(rules) { 365 | return P.regex(/countof\s+/ui) 366 | .map((result) => { 367 | return { 368 | type: 'CountofOperator', 369 | value: result.toLowerCase().trim(), 370 | }; 371 | }); 372 | }, 373 | }); 374 | }; 375 | -------------------------------------------------------------------------------- /src/util/TraceLogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, logger } from 'vscode-debugadapter'; 2 | import { now } from './util'; 3 | 4 | export class TraceLogger { 5 | public enable = false; 6 | constructor(logCallback: (e) => void) { 7 | logger.init(logCallback); 8 | logger.setup(Logger.LogLevel.Log); 9 | } 10 | public log(message: string): void { 11 | if (this.enable) { 12 | const _message = `${now()} Trace ${message}`; 13 | logger.log(_message); 14 | console.log(_message); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/util/VariableManager.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as DebugAdapter from 'vscode-debugadapter'; 3 | import { DebugProtocol } from 'vscode-debugprotocol'; 4 | import { URI } from 'vscode-uri'; 5 | import * as dbgp from '../dbgpSession'; 6 | import { rtrim } from 'underscore.string'; 7 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 8 | import { isNumberLike, isPrimitive, toArray } from './util'; 9 | import { equalsIgnoreCase } from './stringUtils'; 10 | import { CategoryData, MatcherData, ScopeSelector } from '../extension'; 11 | import { CaseInsensitiveMap } from './CaseInsensitiveMap'; 12 | import { AhkDebugSession } from '../ahkDebug'; 13 | 14 | export const escapeAhk = (str: string, ahkVersion?: AhkVersion): string => { 15 | return str 16 | .replace(/"/gu, ahkVersion?.mejor === 2 ? '`"' : '""') 17 | .replace(/\r\n/gu, '`r`n') 18 | .replace(/\n/gu, '`n') 19 | .replace(/\r/gu, '`r') 20 | .replace(/[\b]/gu, '`b') 21 | .replace(/\t/gu, '`t') 22 | .replace(/\v/gu, '`v') 23 | // eslint-disable-next-line no-control-regex 24 | .replace(/[\x07]/gu, '`a') 25 | .replace(/\f/gu, '`f'); 26 | }; 27 | export const unescapeAhk = (str: string, ahkVersion?: AhkVersion): string => { 28 | return str 29 | .replace(ahkVersion?.mejor === 2 ? /`"/gu : /""/gu, '"') 30 | .replace(/`r`n/gu, '\r\n') 31 | .replace(/`n/gu, '\n') 32 | .replace(/`r/gu, '\r') 33 | .replace(/`b/gu, '\b') 34 | .replace(/`t/gu, '\t') 35 | .replace(/`v/gu, '\v') 36 | .replace(/`f/gu, '\f'); 37 | }; 38 | export const formatProperty = (property: dbgp.Property, ahkVersion?: AhkVersion): string => { 39 | const formatPrimitiveProperty = (property: dbgp.PrimitiveProperty): string => { 40 | if (property.type === 'string') { 41 | return `"${escapeAhk(property.value, ahkVersion)}"`; 42 | } 43 | else if (property.type === 'undefined') { 44 | return 'Not initialized'; 45 | } 46 | return property.value; 47 | }; 48 | 49 | if (property instanceof dbgp.PrimitiveProperty) { 50 | return formatPrimitiveProperty(property); 51 | } 52 | 53 | const objectProperty = property as dbgp.ObjectProperty; 54 | const maxIndex = objectProperty.maxIndex; 55 | const isArray = objectProperty.isArray; 56 | let value = isArray 57 | ? `${objectProperty.className}(${maxIndex!}) [` 58 | : `${objectProperty.className} {`; 59 | 60 | const children = objectProperty.children.slice(0, 100).filter((property) => property.name !== ''); 61 | for (const child of children) { 62 | const displayValue = child instanceof dbgp.PrimitiveProperty 63 | ? formatPrimitiveProperty(child) 64 | : (child as dbgp.ObjectProperty).className; 65 | 66 | const objectChild = child as dbgp.ObjectProperty; 67 | if (objectProperty.isArray) { 68 | if (!objectChild.isIndexKey) { 69 | continue; 70 | } 71 | 72 | value += `${displayValue}, `; 73 | continue; 74 | } 75 | 76 | const key = objectChild.isIndexKey 77 | ? String(objectChild.index) 78 | : objectChild.name; 79 | value += `${key}: ${displayValue}, `; 80 | } 81 | 82 | if (children.length === 100) { 83 | value += '…'; 84 | } 85 | 86 | value = rtrim(value, ', '); 87 | value += isArray ? ']' : '}'; 88 | return value; 89 | }; 90 | export const isComObject = (property: dbgp.ObjectProperty): boolean => { 91 | const comPropertyInfos = [ 92 | [ 'Value', 'integer' ], 93 | [ 'VarType', 'integer' ], 94 | [ 'DispatchType', 'string' ], 95 | [ 'DispatchIID', 'string' ], 96 | ]; 97 | 98 | if (property.children.length !== 4) { 99 | return false; 100 | } 101 | for (const comPropertyInfo of comPropertyInfos) { 102 | const [ expectedName, expectedType ] = comPropertyInfo; 103 | const comProperty = property.children.find((child) => child.name === expectedName); 104 | if (!comProperty) { 105 | return false; 106 | } 107 | if (comProperty.type !== expectedType) { 108 | return false; 109 | } 110 | } 111 | return true; 112 | }; 113 | 114 | const handles = new DebugAdapter.Handles(); 115 | 116 | export type StackFrames = StackFrame[] & { isIdleMode?: boolean }; 117 | export class StackFrame implements DebugProtocol.StackFrame { 118 | public readonly dbgpStackFrame: dbgp.StackFrame; 119 | public readonly id: number; 120 | public readonly source: DebugAdapter.Source; 121 | public readonly line: number; 122 | public readonly name: string; 123 | public readonly column = 1; 124 | public readonly session: dbgp.Session; 125 | constructor(session: dbgp.Session, dbgpStackFrame: dbgp.StackFrame) { 126 | this.dbgpStackFrame = dbgpStackFrame; 127 | this.id = handles.create(this); 128 | this.name = dbgpStackFrame.name; 129 | this.line = dbgpStackFrame.line; 130 | const filePath = URI.parse(dbgpStackFrame.fileUri).fsPath; 131 | this.source = new DebugAdapter.Source(path.basename(filePath), filePath); 132 | 133 | this.session = session; 134 | } 135 | } 136 | 137 | export class Scope implements DebugAdapter.Scope { 138 | public readonly session: dbgp.Session; 139 | public readonly context: dbgp.Context; 140 | public readonly name: string; 141 | public readonly variablesReference: number; 142 | public readonly expensive: boolean; 143 | public children?: Variable[]; 144 | constructor(session: dbgp.Session, context: dbgp.Context, expensive = false) { 145 | this.session = session; 146 | this.context = context; 147 | this.name = context.name; 148 | this.variablesReference = handles.create(this); 149 | this.expensive = expensive; 150 | } 151 | public async loadChildren(maxDepth?: number): Promise { 152 | this.children = await this.createChildren(maxDepth); 153 | return this.children; 154 | } 155 | public async createChildren(maxDepth?: number): Promise { 156 | if (!maxDepth && this.children) { 157 | return this.children; 158 | } 159 | 160 | const { properties } = await this.session.sendContextGetCommand(this.context, maxDepth); 161 | return properties.map((property) => { 162 | return new Variable(this.session, property); 163 | }); 164 | } 165 | } 166 | export type CategoryMatcher = (variable: Variable) => boolean; 167 | export class Category implements Scope { 168 | public readonly scopes: Scope[]; 169 | public readonly categoryData: CategoryData; 170 | public readonly allCategoriesData: CategoryData[]; 171 | public readonly categoryMatcher: CategoryMatcher; 172 | public readonly variablesReference: number; 173 | public readonly expensive: boolean; 174 | public children?: Variable[]; 175 | public get context(): dbgp.Context { 176 | return this.scopes[0].context; 177 | } 178 | public get session(): dbgp.Session { 179 | return this.scopes[0].session; 180 | } 181 | public get name(): string { 182 | return this.categoryData.label; 183 | } 184 | constructor(scopes: Scope[], categoryData: CategoryData, allCategoriesData: CategoryData[], expensive = false) { 185 | this.variablesReference = handles.create(this); 186 | this.expensive = expensive; 187 | this.scopes = scopes; 188 | this.categoryData = categoryData; 189 | this.categoryMatcher = this.createCategoryMatcher(categoryData.matchers); 190 | this.allCategoriesData = allCategoriesData; 191 | } 192 | public create(value: Variable | Scope | Category): number { 193 | return handles.create(value); 194 | } 195 | public async loadChildren(maxDepth?: number): Promise { 196 | this.children = await this.createChildren(maxDepth); 197 | return this.children; 198 | } 199 | public async createChildren(maxDepth?: number): Promise { 200 | if (!maxDepth && this.children) { 201 | return this.children; 202 | } 203 | 204 | const sourceScopes = this.scopes.filter((scope) => { 205 | const sourceNames = toArray(this.categoryData.source); 206 | return sourceNames.some((sourceName) => equalsIgnoreCase(scope.name, sourceName)); 207 | }); 208 | 209 | const sourceVariables: Variable[] = []; 210 | for await (const scope of sourceScopes) { 211 | sourceVariables.push(...await scope.createChildren(maxDepth)); 212 | } 213 | 214 | const matchers = this.categoryData.matchers; 215 | if (!matchers && !this.categoryData.noduplicate) { 216 | return sourceVariables; 217 | } 218 | 219 | const variables: Variable[] = sourceVariables.filter((variable) => { 220 | if (this.categoryData.noduplicate) { 221 | const categoriesDataBySameSource = this.allCategoriesData.filter((categoryData) => { 222 | if (this.categoryData.label === categoryData.label) { 223 | return false; 224 | } 225 | if (!categoryData.matchers) { 226 | return false; 227 | } 228 | const sourceA = toArray(this.categoryData.source).sort((a, b) => a.localeCompare(b)).join(); 229 | const sourceB = toArray(categoryData.source).sort((a, b) => a.localeCompare(b)).join(); 230 | return sourceA === sourceB; 231 | }); 232 | const isDuplicated = categoriesDataBySameSource.some((categoryData) => this.createCategoryMatcher(categoryData.matchers)(variable)); 233 | if (isDuplicated) { 234 | return false; 235 | } 236 | } 237 | return this.categoryMatcher(variable); 238 | }); 239 | return variables.sort((a, b) => { 240 | if (a.property.isIndexKey && b.property.isIndexKey) { 241 | return a.property.index! - b.property.index!; 242 | } 243 | return a.name.localeCompare(b.name); 244 | }); 245 | } 246 | private createCategoryMatcher(matchersData?: MatcherData[]): CategoryMatcher { 247 | if (!matchersData || matchersData.length === 0) { 248 | return (variable: Variable): boolean => true; 249 | } 250 | return (variable: Variable): boolean => { 251 | return matchersData.every((matcher) => { 252 | const matchers: Array<(() => boolean)> = []; 253 | if (typeof matcher.pattern === 'string') { 254 | const regex = new RegExp(matcher.pattern, matcher.ignorecase ? 'iu' : 'u'); 255 | matchers.push(() => regex.test(variable.name)); 256 | } 257 | if (typeof matcher.builtin === 'boolean') { 258 | matchers.push(() => { 259 | if (variable.property.facet === 'Builtin') { 260 | return true; 261 | } 262 | if ((/(^A_)|^\d$/ui).test(variable.name)) { 263 | return true; 264 | } 265 | 266 | const globalVariableNames = this.session.ahkVersion.mejor === 2 267 | ? [ 'Abs', 'ACos', 'Any', 'Array', 'ASin', 'ATan', 'BlockInput', 'BoundFunc', 'Break', 'Buffer', 'CallbackCreate', 'CallbackFree', 'CaretGetPos', 'Catch', 'Ceil', 'Chr', 'Class', 'Click', 'ClipboardAll', 'ClipWait', 'Closure', 'ComCall', 'ComObjActive', 'ComObjArray', 'ComObjConnect', 'ComObject', 'ComObjFlags', 'ComObjFromPtr', 'ComObjGet', 'ComObjQuery', 'ComObjType', 'ComObjValue', 'ComValue', 'ComValueRef', 'Continue', 'ControlAddItem', 'ControlChooseIndex', 'ControlChooseString', 'ControlClick', 'ControlDeleteItem', 'ControlFindItem', 'ControlFocus', 'ControlGetChecked', 'ControlGetChoice', 'ControlGetClassNN', 'ControlGetEnabled', 'ControlGetExStyle', 'ControlGetFocus', 'ControlGetHwnd', 'ControlGetIndex', 'ControlGetItems', 'ControlGetPos', 'ControlGetStyl', 'ControlGetText', 'ControlGetVisible', 'ControlHide', 'ControlHideDropDown', 'ControlMove', 'ControlSen', 'ControlSendText', 'ControlSetChecked', 'ControlSetEnabled', 'ControlSetExStyle', 'ControlSetStyl', 'ControlSetText', 'ControlShow', 'ControlShowDropDown', 'CoordMode', 'Cos', 'Critical', 'DateAdd', 'DateDiff', 'DetectHiddenText', 'DetectHiddenWindows', 'DirCopy', 'DirCreate', 'DirDelete', 'DirExist', 'DirMove', 'DirSelect', 'DllCall', 'Download', 'DriveEject', 'DriveGetCapacity', 'DriveGetFileSystem', 'DriveGetLabel', 'DriveGetList', 'DriveGetSerial', 'DriveGetSpaceFree', 'DriveGetStatus', 'DriveGetStatusCD', 'DriveGetType', 'DriveLock', 'DriveRetract', 'DriveSetLabel', 'DriveUnlock', 'Edit', 'EditGetCurrentCol', 'EditGetCurrentLine', 'EditGetLine', 'EditGetLineCount', 'EditGetSelectedText', 'EditPaste', 'Else', 'Enumerator', 'EnvGet', 'EnvSet', 'Error', 'Exit', 'ExitApp', 'Exp', 'File', 'FileAppend', 'FileCopy', 'FileCreateShortcut', 'FileDelete', 'FileEncoding', 'FileExist', 'FileGetAttrib', 'FileGetShortcut', 'FileGetSize', 'FileGetTime', 'FileGetVersion', 'FileInstall', 'FileMove', 'FileOpen', 'FileRead', 'FileRecycle', 'FileRecycleEmpty', 'FileSelect', 'FileSetAttrib', 'FileSetTime', 'Finally', 'Float', 'Floor', 'For', 'Format', 'FormatTime', 'Func', 'GetKeyName', 'GetKeySC', 'GetKeyState', 'GetKeyVK', 'GetMethod', 'Goto', 'GroupActivate', 'GroupAdd', 'GroupClose', 'GroupDeactivate', 'Gui', 'Gui()', 'GuiCtrlFromHwnd', 'GuiFromHwnd', 'HasBase', 'HasMethod', 'HasProp', 'HotIf', 'Hotkey', 'Hotstring', 'If', 'IL_Ad', 'IL_Creat', 'IL_Destroy', 'ImageSearch', 'IndexError', 'IniDelete', 'IniRead', 'IniWrite', 'InputBox', 'InputHook', 'InstallKeybdHook', 'InstallMouseHook', 'InStr', 'Integer', 'IsLabel', 'IsObject', 'IsSet', 'KeyError', 'KeyHistory', 'KeyWait', 'ListHotkeys', 'ListLines', 'ListVars', 'ListViewGetContent', 'Ln', 'LoadPicture', 'Log', 'Loop', 'Map', 'Max', 'MemberError', 'MemoryError', 'Menu', 'Menu()', 'MenuBar', 'MenuBar()', 'MenuFromHandle', 'MenuSelect', 'MethodError', 'Min', 'Mod', 'MonitorGet', 'MonitorGetCount', 'MonitorGetName', 'MonitorGetPrimary', 'MonitorGetWorkArea', 'MouseClick', 'MouseClickDrag', 'MouseGetPos', 'MouseMove', 'MsgBox', 'Number', 'NumGet', 'NumPut', 'ObjAddRef', 'ObjAddress', 'ObjBindMethod', 'Object', 'ObjGetBase', 'ObjGetCapacity', 'ObjHasOwnPro', 'ObjOwnProp', 'ObjOwnPropCount', 'ObjPtr', 'ObjRelease', 'ObjSetBase', 'ObjSetCapacity', 'OnClipboardChange', 'OnError', 'OnExit', 'OnMessage', 'Ord', 'OSError', 'OutputDebug', 'Pause', 'Persistent', 'PixelGetColor', 'PixelSearch', 'PostMessage', 'Primitive', 'ProcessClose', 'ProcessExist', 'ProcessSetPriority', 'ProcessWait', 'ProcessWaitClose', 'PropertyError', 'Random', 'RegDelete', 'RegDeleteKey', 'RegExMatch', 'RegExMatchInfo', 'RegExReplace', 'RegRead', 'RegWrite', 'Reload', 'Return', 'Round', 'Run', 'RunAs', 'RunWait', 'Send', 'SendLevel', 'SendMessage', 'SendMode', 'SetCapsLockState', 'SetControlDelay', 'SetDefaultMouseSpeed', 'SetKeyDelay', 'SetMouseDelay', 'SetNumLockState', 'SetRegView', 'SetScrollLockState', 'SetStoreCapsLockMode', 'SetTimer', 'SetTitleMatchMode', 'SetWinDelay', 'SetWorkingDir', 'Shutdown', 'Sin', 'Sleep', 'Sort', 'SoundBeep', 'SoundGetInterface', 'SoundGetMute', 'SoundGetName', 'SoundGetVolume', 'SoundPlay', 'SoundSetMute', 'SoundSetVolume', 'SplitPath', 'Sqrt', 'StatusBarGetText', 'StatusBarWait', 'StrCompare', 'StrGet', 'String', 'StrLen', 'StrLower', 'StrPut', 'StrReplace', 'StrSplit', 'StrUpper', 'SubStr', 'Suspend', 'Switch', 'SysGet', 'SysGetIPAddresses', 'Tan', 'TargetError', 'These', 'Thread', 'Throw', 'TimeoutError', 'ToolTip', 'TraySetIcon', 'TrayTip', 'Trim', 'Try', 'Type', 'TypeError', 'Until', 'ValueError', 'VarRef', 'VarSetStrCapacity', 'VerCompare', 'While-loop', 'WinActivate', 'WinActivateBottom', 'WinActive', 'WinClose', 'WinExist', 'WinGetClass', 'WinGetClientPos', 'WinGetControls', 'WinGetControlsHwnd', 'WinGetCount', 'WinGetExStyle', 'WinGetID', 'WinGetIDLast', 'WinGetList', 'WinGetMinMax', 'WinGetPID', 'WinGetPos', 'WinGetProcessName', 'WinGetProcessPath', 'WinGetStyl', 'WinGetText', 'WinGetTitle', 'WinGetTransColor', 'WinGetTransparent', 'WinHide', 'WinKill', 'WinMaximize', 'WinMinimize', 'WinMinimizeAll', 'WinMove', 'WinMoveBottom', 'WinMoveTop', 'WinRedraw', 'WinRestore', 'WinSetAlwaysOnTop', 'WinSetEnabled', 'WinSetExStyle', 'WinSetRegion', 'WinSetStyl', 'WinSetTitle', 'WinSetTransColor', 'WinSetTransparent', 'WinShow', 'WinWait', 'WinWaitActive', 'WinWaitClose', 'ZeroDivisionError' ] 268 | : [ 'ErrorLevel' ]; 269 | const isBuiltin = globalVariableNames.some((name) => equalsIgnoreCase(name, variable.name)); 270 | 271 | return matcher.builtin ? isBuiltin : !isBuiltin; 272 | }); 273 | } 274 | if (typeof matcher.static === 'boolean') { 275 | matchers.push(() => { 276 | const isStatic = variable.property.facet === 'Static'; 277 | return matcher.static ? isStatic : !isStatic; 278 | }); 279 | } 280 | if (typeof matcher.type === 'string') { 281 | matchers.push(() => variable.type === matcher.type); 282 | } 283 | if (typeof matcher.className === 'string') { 284 | matchers.push(() => Boolean(variable.className && equalsIgnoreCase(variable.className, matcher.className!))); 285 | } 286 | 287 | const result = matchers.every((tester) => tester()); 288 | if (matcher.method === 'exclude') { 289 | return !result; 290 | } 291 | return result; 292 | }); 293 | }; 294 | } 295 | } 296 | export class Categories extends Array implements DebugProtocol.Variable { 297 | public readonly name: string; 298 | public readonly value: string; 299 | public readonly expensive: boolean; 300 | public readonly variablesReference: number; 301 | public children?: DebugProtocol.Variable[]; 302 | constructor(...params: Array) { 303 | super(...params); 304 | 305 | this.name = 'VARIABLES'; 306 | this.value = 'VARIABLES'; 307 | this.variablesReference = handles.create(this); 308 | this.expensive = false; 309 | } 310 | public async loadChildren(maxDepth?: number): Promise { 311 | this.children = await this.createChildren(maxDepth); 312 | return this.children; 313 | } 314 | public async createChildren(maxDepth?: number): Promise { 315 | if (!maxDepth && this.children) { 316 | return this.children; 317 | } 318 | 319 | const variables: DebugProtocol.Variable[] = []; 320 | for await (const scope of this) { 321 | await scope.loadChildren(maxDepth); 322 | variables.push({ 323 | name: scope.name, 324 | value: scope.name, 325 | variablesReference: scope.variablesReference, 326 | }); 327 | } 328 | return variables; 329 | } 330 | } 331 | export class VariableGroup extends Array implements DebugProtocol.Variable { 332 | public name = ''; 333 | public value = ''; 334 | public readonly variablesReference: number; 335 | constructor(...variables: Array) { 336 | super(); 337 | this.variablesReference = handles.create(this); 338 | this.push(...variables); 339 | } 340 | } 341 | export class Variable implements DebugProtocol.Variable { 342 | public readonly hasChildren: boolean; 343 | public _isLoadedChildren: boolean; 344 | public get isLoadedChildren(): boolean { 345 | return this._isLoadedChildren; 346 | } 347 | public readonly session: dbgp.Session; 348 | public readonly name: string; 349 | public get value(): string { 350 | return formatProperty(this.property, this.session.ahkVersion); 351 | } 352 | public readonly variablesReference: number; 353 | public readonly __vscodeVariableMenuContext: 'string' | 'number' | 'object'; 354 | public readonly type?: string; 355 | public get indexedVariables(): number | undefined { 356 | if (this.property instanceof dbgp.ObjectProperty && this.property.isArray && 100 < this.property.maxIndex!) { 357 | return this.property.maxIndex; 358 | } 359 | return undefined; 360 | } 361 | public get namedVariables(): number | undefined { 362 | return this.property instanceof dbgp.ObjectProperty ? 1 : undefined; 363 | } 364 | public get isArray(): boolean { 365 | return this.property instanceof dbgp.ObjectProperty ? this.property.isArray : false; 366 | } 367 | public _property: dbgp.Property; 368 | public get property(): dbgp.Property { 369 | return this._property; 370 | } 371 | public get context(): dbgp.Context { 372 | return this.property.context; 373 | } 374 | public get fullName(): string { 375 | return this.property.fullName; 376 | } 377 | public get className(): string | undefined { 378 | return this.property instanceof dbgp.ObjectProperty ? this.property.className : undefined; 379 | } 380 | public get maxIndex(): number | undefined { 381 | return this.property instanceof dbgp.ObjectProperty ? this.property.maxIndex : undefined; 382 | } 383 | public get children(): dbgp.Property[] | undefined { 384 | return this.property instanceof dbgp.ObjectProperty ? this.property.children : undefined; 385 | } 386 | constructor(session: dbgp.Session, property: dbgp.Property) { 387 | this.hasChildren = property instanceof dbgp.ObjectProperty; 388 | this._isLoadedChildren = property instanceof dbgp.ObjectProperty && 0 < property.children.length; 389 | 390 | this.session = session; 391 | this._property = property; 392 | this.name = property.name; 393 | this.variablesReference = this.hasChildren ? handles.create(this) : 0; 394 | this.type = property.type; 395 | if (property instanceof dbgp.PrimitiveProperty) { 396 | this.__vscodeVariableMenuContext = isNumberLike(property.value) ? 'number' : 'string'; 397 | } 398 | else { 399 | this.__vscodeVariableMenuContext = 'object'; 400 | } 401 | } 402 | public async loadChildren(): Promise { 403 | if (!this.isLoadedChildren) { 404 | const reloadedProperty = await this.session.safeFetchProperty(this.context, this.fullName, 1); 405 | if (reloadedProperty) { 406 | this._property = reloadedProperty; 407 | this._isLoadedChildren = true; 408 | } 409 | } 410 | } 411 | public async createMembers(args: DebugProtocol.VariablesArguments): Promise { 412 | if (!(this.property instanceof dbgp.ObjectProperty)) { 413 | return undefined; 414 | } 415 | 416 | await this.loadChildren(); 417 | if (!this.children) { 418 | return undefined; 419 | } 420 | 421 | const variables: Variable[] = []; 422 | for await (const property of this.children) { 423 | // Fix: [#133](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/133) 424 | if (property.fullName.includes('')) { 425 | continue; 426 | } 427 | 428 | if (args.filter) { 429 | if (args.filter === 'named' && property.isIndexKey) { 430 | continue; 431 | } 432 | if (args.filter === 'indexed') { 433 | if (!property.isIndexKey) { 434 | continue; 435 | } 436 | const index = property.index!; 437 | const start = args.start! + 1; 438 | const end = args.start! + args.count!; 439 | const contains = start <= index && index <= end; 440 | if (!contains) { 441 | continue; 442 | } 443 | } 444 | } 445 | 446 | const variable = new Variable(this.session, property); 447 | if (!isComObject(this.property)) { 448 | await variable.loadChildren(); 449 | } 450 | variables.push(variable); 451 | } 452 | return variables; 453 | } 454 | } 455 | export type MetaVariableValue = string | number | Scope | Category | Categories | StackFrame | StackFrames | Record; 456 | export type LazyMetaVariableValue = Promise; 457 | export class MetaVariable implements DebugProtocol.Variable { 458 | public readonly name: string; 459 | public readonly type = 'metavariable'; 460 | public readonly variablesReference: number; 461 | public get indexedVariables(): number | undefined { 462 | if (this.rawValue instanceof Variable) { 463 | return this.rawValue.indexedVariables; 464 | } 465 | return undefined; 466 | } 467 | public get namedVariables(): number | undefined { 468 | if (this.rawValue instanceof Variable) { 469 | return this.rawValue.namedVariables; 470 | } 471 | return undefined; 472 | } 473 | public rawValue: MetaVariableValue | LazyMetaVariableValue; 474 | public loadedPromiseValue?: MetaVariableValue; 475 | public children?: Array; 476 | public get value(): string { 477 | const value = this.loadedPromiseValue ?? this.rawValue; 478 | if (isPrimitive(value)) { 479 | return typeof this.rawValue === 'string' ? `"${value}"` : String(value); 480 | } 481 | if (value instanceof Scope || value instanceof Category || value instanceof Categories || value instanceof StackFrame) { 482 | return value.name; 483 | } 484 | if (value instanceof Variable || value instanceof MetaVariable) { 485 | return value.value; 486 | } 487 | return JSON.stringify(value); 488 | } 489 | public get hasChildren(): boolean { 490 | return !isPrimitive(this.loadedPromiseValue ?? this.rawValue); 491 | } 492 | constructor(name: string, value: MetaVariableValue | LazyMetaVariableValue) { 493 | this.name = name; 494 | this.rawValue = value; 495 | this.variablesReference = isPrimitive(value) ? 0 : handles.create(this); 496 | } 497 | public async loadChildren(maxDepth?: number): Promise> { 498 | this.children = await this.createChildren(maxDepth); 499 | return this.children; 500 | } 501 | public async createChildren(maxDepth?: number): Promise> { 502 | if (this.children) { 503 | return this.children; 504 | } 505 | if (!this.loadedPromiseValue) { 506 | this.loadedPromiseValue = this.rawValue instanceof Promise ? await this.rawValue : this.rawValue; 507 | } 508 | 509 | if (isPrimitive(this.loadedPromiseValue)) { 510 | return []; 511 | } 512 | if (this.loadedPromiseValue instanceof Scope || this.loadedPromiseValue instanceof Category || this.loadedPromiseValue instanceof Categories || this.loadedPromiseValue instanceof MetaVariable) { 513 | return this.loadedPromiseValue.createChildren(maxDepth); 514 | } 515 | return Object.entries(this.loadedPromiseValue).map(([ key, value ]) => { 516 | if (value instanceof MetaVariable) { 517 | return value; 518 | } 519 | return new MetaVariable(key, value); 520 | }); 521 | } 522 | } 523 | export class MetaVariableValueMap extends CaseInsensitiveMap { 524 | public createMetaVariable(name: string): MetaVariable | undefined { 525 | const value = this.get(name); 526 | if (!value) { 527 | return undefined; 528 | } 529 | return new MetaVariable(name, value); 530 | } 531 | // public get(key: string): MetaVariableValue | LazyMetaVariableValue | undefined { 532 | // return super.get(key); 533 | // } 534 | // public set(key: string, value: MetaVariableValue | LazyMetaVariableValue): this { 535 | // return super.set(key, value); 536 | // } 537 | } 538 | 539 | export class VariableManager { 540 | public readonly debugAdapter: AhkDebugSession; 541 | public readonly session: dbgp.Session; 542 | public readonly categoriesData?: CategoryData[]; 543 | // private readonly scopeByVariablesReference = new Map(); 544 | // private readonly objectByVariablesReference = new Map(); 545 | // private readonly stackFramesByFrameId = new Map(); 546 | constructor(debugAdapter: AhkDebugSession, categories?: CategoryData[]) { 547 | this.debugAdapter = debugAdapter; 548 | this.session = debugAdapter.session!; 549 | this.categoriesData = categories; 550 | } 551 | public createVariableReference(value?: any): number { 552 | return handles.create(value); 553 | } 554 | public async createCategories(frameId: number): Promise { 555 | const defaultScopes = await this.createDefaultScopes(frameId); 556 | if (!this.categoriesData) { 557 | return defaultScopes; 558 | } 559 | 560 | const categories = new Categories(); 561 | for await (const categoryData of this.categoriesData) { 562 | if (categoryData.hidden === true) { 563 | continue; 564 | } 565 | 566 | const existSources = defaultScopes.some((scope) => { 567 | return toArray(categoryData.source).some((source) => equalsIgnoreCase(scope.name, source)); 568 | }); 569 | if (!existSources) { 570 | continue; 571 | } 572 | 573 | const category = new Category(defaultScopes, categoryData, this.categoriesData); 574 | if (categoryData.hidden === 'auto') { 575 | await category.loadChildren(); 576 | if (category.children?.length === 0) { 577 | continue; 578 | } 579 | } 580 | categories.push(category); 581 | } 582 | return categories; 583 | } 584 | public getCategory(variablesReference: number): Scope | Category | undefined { 585 | const category = handles.get(variablesReference); 586 | if (category instanceof Scope || category instanceof Category) { 587 | return category; 588 | } 589 | return undefined; 590 | } 591 | public getCategories(variablesReference: number): Categories | undefined { 592 | const categories = handles.get(variablesReference); 593 | if (categories instanceof Categories) { 594 | return categories; 595 | } 596 | return undefined; 597 | } 598 | public getObjectVariable(variablesReference: number): Variable | undefined { 599 | const variable = handles.get(variablesReference); 600 | if (variable instanceof Variable) { 601 | return variable; 602 | } 603 | return undefined; 604 | } 605 | public getMetaVariable(variablesReference: number): MetaVariable | undefined { 606 | const metaVariable = handles.get(variablesReference); 607 | if (metaVariable instanceof MetaVariable) { 608 | return metaVariable; 609 | } 610 | return undefined; 611 | } 612 | public async createVariables(args: DebugProtocol.VariablesArguments, maxDepth?: number): Promise { 613 | const variable = this.getObjectVariable(args.variablesReference); 614 | if (variable) { 615 | return variable.createMembers(args); 616 | } 617 | const scope = this.getCategory(args.variablesReference); 618 | return scope?.createChildren(maxDepth); 619 | } 620 | public async createStackFrames(): Promise { 621 | const { stackFrames: dbgpStackFrames } = await this.session.sendStackGetCommand(); 622 | 623 | const stackFrames: StackFrames = dbgpStackFrames.map((dbgpStackFrame) => { 624 | return new StackFrame(this.session, dbgpStackFrame); 625 | }); 626 | stackFrames.isIdleMode = false; 627 | if (dbgpStackFrames.length === 0) { 628 | stackFrames.isIdleMode = true; 629 | stackFrames.push(new StackFrame( 630 | this.session, 631 | { 632 | name: 'Standby', 633 | fileUri: URI.file(this.debugAdapter.config.program).path, 634 | level: 0, 635 | line: 0, 636 | type: 'file', 637 | }, 638 | )); 639 | } 640 | return stackFrames; 641 | } 642 | public getStackFrame(frameId: number): StackFrame | undefined { 643 | const stackFrame = handles.get(frameId); 644 | if (stackFrame instanceof StackFrame) { 645 | return stackFrame; 646 | } 647 | return undefined; 648 | } 649 | public async evaluate(name: string, stackFrame?: dbgp.StackFrame): Promise { 650 | const property = await this.session.evaluate(name, stackFrame); 651 | if (!property) { 652 | return undefined; 653 | } 654 | return new Variable(this.session, property); 655 | } 656 | private async createDefaultScopes(frameId: number): Promise { 657 | const stackFrame = this.getStackFrame(frameId) ?? (await this.createStackFrames())[0]; 658 | const { contexts } = await this.session.sendContextNamesCommand(stackFrame.dbgpStackFrame); 659 | const scopes = new Categories(); 660 | for await (const context of contexts) { 661 | const scope = new Scope(this.session, context); 662 | scopes.push(scope); 663 | } 664 | return scopes; 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /src/util/getAhkVersion.ts: -------------------------------------------------------------------------------- 1 | import { SpawnSyncOptions, spawnSync } from 'child_process'; 2 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 3 | 4 | export const getAhkVersion = (ahkRuntime: string, options?: SpawnSyncOptions): AhkVersion | null => { 5 | const ahkCode = 'FileOpen("*", "w").write(A_AhkVersion)'; 6 | const result = spawnSync(ahkRuntime, [ '/ErrorStdOut', '*' ], { ...options, input: ahkCode }); 7 | if (result.error) { 8 | return null; 9 | } 10 | const version = result.stdout.toString(); 11 | return new AhkVersion(version); 12 | }; 13 | -------------------------------------------------------------------------------- /src/util/getRunningAhkScriptList.ts: -------------------------------------------------------------------------------- 1 | import { SpawnSyncOptions, spawnSync } from 'child_process'; 2 | import { getAhkVersion } from './getAhkVersion'; 3 | 4 | export const getRunningAhkScriptList = (ahkRuntime: string, options?: SpawnSyncOptions): string[] => { 5 | const version = getAhkVersion(ahkRuntime); 6 | if (!version) { 7 | return []; 8 | } 9 | const ahkCode = version.mejor <= 1.1 10 | ? ` 11 | DetectHiddenWindows On 12 | WinGet idList, List, ahk_class AutoHotkey 13 | 14 | processPathList := "" 15 | Loop %idList% { 16 | id := idList%A_Index% 17 | WinGetTitle title, ahk_id %id% 18 | if (title ~= "^.+\\\\\\*(?=\\s-)") { 19 | continue 20 | } 21 | RegExMatch(title, "O)(?.+)(?=\\s-)", match) 22 | processPathList .= match.path "\`n" 23 | } 24 | FileOpen("*", "w").write(RTrim(processPathList, "\`n")) 25 | ` : ` 26 | A_DetectHiddenWindows := true 27 | idList := WinGetList("ahk_class AutoHotkey") 28 | 29 | processPathList := "" 30 | for i, id in idList { 31 | title := WinGetTitle("ahk_id " id) 32 | if (title ~= "^\\*(?=\\s-)") { 33 | continue 34 | } 35 | RegExMatch(title, "(?.+)(?=\\s-)", &match) 36 | processPathList .= match.path "\`n" 37 | } 38 | FileOpen("*", "w").write(RTrim(processPathList, "\`n")) 39 | `; 40 | const result = spawnSync(ahkRuntime, [ '/ErrorStdOut', '*' ], { ...options, input: ahkCode }); 41 | if (result.error) { 42 | return []; 43 | } 44 | const scriptList = result.stdout.toString(); 45 | if (scriptList) { 46 | return scriptList.split('\n'); 47 | } 48 | return []; 49 | }; 50 | -------------------------------------------------------------------------------- /src/util/numberUtils.ts: -------------------------------------------------------------------------------- 1 | export const toFixed = (number: number, precision: number): string => { 2 | if (Number.isInteger(number)) { 3 | return String(number); 4 | } 5 | 6 | const number_str = String(number); 7 | const afterPoint = number_str.slice(number_str.indexOf('.') + 1); 8 | 9 | const splited = afterPoint.split(''); 10 | let newPrecision = 0; 11 | for (let i = 0; i < splited.length; i++) { 12 | if (splited[i] === '0') { 13 | continue; 14 | } 15 | 16 | newPrecision = i + precision; 17 | break; 18 | } 19 | return number.toFixed(newPrecision); 20 | }; 21 | -------------------------------------------------------------------------------- /src/util/stringUtils.ts: -------------------------------------------------------------------------------- 1 | export const equalsIgnoreCase = (a: string, b: string): boolean => { 2 | const _a = a.toLowerCase(); 3 | const _b = b.toLowerCase(); 4 | return _a === _b; 5 | }; 6 | export const startsWithIgnoreCase = (str: string, prefix: string): boolean => { 7 | const _str = str.toLowerCase(); 8 | const _preifx = prefix.toLowerCase(); 9 | return _str.startsWith(_preifx); 10 | }; 11 | export const lastIndexOf = (str: string, searchText: string | RegExp, fromIndex?: number): number => { 12 | if (typeof searchText === 'string') { 13 | return str.lastIndexOf(searchText, fromIndex); 14 | } 15 | const fixedString = fromIndex ? str.substr(fromIndex) : str; 16 | const regexp = new RegExp(searchText.source, `${searchText.flags}g`); 17 | const result = [ ...fixedString.matchAll(regexp) ].pop(); 18 | return result?.index ?? -1; 19 | }; 20 | 21 | export const escapePcreRegExEscape = (str: string): string => { 22 | return str 23 | .replace(/\\/gu, '\\\\') 24 | .replace(/\./gu, '\\.') 25 | .replace(/\*/gu, '\\*') 26 | .replace(/\?/gu, '\\?') 27 | .replace(/\+/gu, '\\+') 28 | .replace(/\[/gu, '\\[') 29 | .replace(/\{/gu, '\\{') 30 | .replace(/\|/gu, '\\|') 31 | .replace(/\(/gu, '\\(') 32 | .replace(/\)/gu, '\\)') 33 | .replace(/\^/gu, '\\^') 34 | .replace(/\$/gu, '\\$'); 35 | }; 36 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 3 | import { statSync } from 'fs'; 4 | import { isArray } from 'ts-predicates'; 5 | 6 | export const isDirectory = (dirPath): boolean => { 7 | try { 8 | return statSync(dirPath).isDirectory(); 9 | } 10 | catch { 11 | } 12 | return false; 13 | }; 14 | export const isPrimitive = (value: any): value is string | number | boolean => { 15 | return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'; 16 | }; 17 | export const isNumberLike = (value: any): boolean => { 18 | if (typeof value === 'string') { 19 | if (value === '' || (/\s+/u).test(value)) { 20 | return false; 21 | } 22 | return Boolean(!isNaN(Number(value.trim())) || parseFloat(value.trim())); 23 | } 24 | if (typeof value === 'number') { 25 | return true; 26 | } 27 | return false; 28 | }; 29 | export const isFloatLike = (value): boolean => { 30 | if (!isNumberLike(value)) { 31 | return false; 32 | } 33 | return String(value).includes('.'); 34 | }; 35 | export const isIntegerLike = (value: any): boolean => { 36 | if (isFloatLike(value)) { 37 | return false; 38 | } 39 | return !isNaN(Number(value)) && Number.isInteger(parseFloat(value)); 40 | }; 41 | export const toArray = (value: any): T[] => { 42 | return (isArray(value) ? value : [ value ]) as T[]; 43 | }; 44 | export const timeoutPromise = async(promise: Promise, timeout: number): Promise => { 45 | return Promise.race([ 46 | promise, new Promise((resolve, reject) => { 47 | const id = setTimeout(() => { 48 | clearTimeout(id); 49 | reject(new Error('timeout')); 50 | }, timeout); 51 | }), 52 | ]); 53 | }; 54 | export const splitVariablePath = (ahkVersion: AhkVersion, variablePath: string): string[] => { 55 | const result: string[] = []; 56 | 57 | const chars = variablePath.split(''); 58 | let part = '', quote: '' | '"' | '\'' = '', bracketCount = 0; 59 | for (let i = 0; i < chars.length; i++) { 60 | const char = chars[i]; 61 | const prevChar = chars[i - 1] as string | undefined; 62 | const nextChar = chars[i + 1] as string | undefined; 63 | // const isIdentifierChar = (): boolean => (ahkVersion === 1 ? /^[\w_@#$]$/u : /^[\w_]$/u).test(char); 64 | 65 | if (quote) { 66 | part += char; 67 | const isEscapeQuote = ( 68 | (2 <= ahkVersion.mejor && char === '`' && nextChar === quote) 69 | || (ahkVersion.mejor <= 1.1 && char === quote && nextChar === quote) 70 | ); 71 | if (isEscapeQuote) { 72 | part += nextChar; 73 | i++; 74 | continue; 75 | } 76 | 77 | if (char === quote) { 78 | quote = ''; 79 | continue; 80 | } 81 | continue; 82 | } 83 | else if (0 < bracketCount && char === ']') { 84 | part += char; 85 | if (bracketCount === 1) { 86 | result.push(part); 87 | part = ''; 88 | } 89 | 90 | bracketCount--; 91 | continue; 92 | } 93 | else if (0 < bracketCount && char === '.') { 94 | part += char; 95 | continue; 96 | } 97 | 98 | switch (char) { 99 | case `'`: { 100 | if (ahkVersion.mejor === 2) { 101 | quote = char; 102 | part += char; 103 | continue; 104 | } 105 | part += char; 106 | continue; 107 | } 108 | case '"': { 109 | quote = char; 110 | part += char; 111 | continue; 112 | } 113 | case '.': { 114 | if (part || prevChar === '.') { 115 | result.push(part); 116 | part = ''; 117 | } 118 | continue; 119 | } 120 | case '[': { 121 | if (bracketCount === 0 && part) { 122 | result.push(part); 123 | part = ''; 124 | } 125 | 126 | part += char; 127 | bracketCount++; 128 | continue; 129 | } 130 | default: { 131 | part += char; 132 | continue; 133 | } 134 | } 135 | } 136 | 137 | if (part !== '') { 138 | result.push(part); 139 | } 140 | const lastChar = chars[chars.length - 1]; 141 | if (lastChar === '.') { 142 | result.push(''); 143 | } 144 | 145 | return result; 146 | }; 147 | export const joinVariablePathArray = (pathArray: string[]): string => { 148 | return pathArray.map((part, i) => { 149 | if (0 < i) { 150 | return part.startsWith('[') ? part : `.${part}`; 151 | } 152 | return part; 153 | }).join(''); 154 | }; 155 | export const now = (): string => { 156 | const now = new Date(); 157 | const month = String(now.getMonth()).padStart(2, '0'); 158 | const date = String(now.getDate()).padStart(2, '0'); 159 | const hours = String(now.getHours()).padStart(2, '0'); 160 | const minutes = String(now.getMinutes()).padStart(2, '0'); 161 | const seconds = String(now.getSeconds()).padStart(2, '0'); 162 | const milliSeconds = String(now.getMilliseconds()).padStart(3, '0'); 163 | return `${now.getFullYear()}/${month}/${date} ${hours}:${minutes}:${seconds}.${milliSeconds}`; 164 | }; 165 | -------------------------------------------------------------------------------- /test/dbgpSession.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as dbgp from '../src/dbgpSession'; 3 | import * as net from 'net'; 4 | 5 | suite('Debug session test', () => { 6 | setup(function(done) { 7 | this.serverSocket = net.connect(9000, 'localhost'); 8 | this.session = new dbgp.Session(this.serverSocket); 9 | this.server = net.createServer((socket) => { 10 | this.socket = socket; 11 | done(); 12 | }).listen(9000, 'localhost'); 13 | }); 14 | 15 | test('handlePacket', function(done) { 16 | const packets = [ 17 | '2', 18 | '17\0', 19 | '', 20 | '\0', 22 | ]; 23 | this.session.on('init', (response: dbgp.InitPacket) => { 24 | assert.deepStrictEqual(response, { 25 | appId: 'AutoHotkey', 26 | fileUri: 'file:///W%3A/project/vscode-autohotkey-debug/demo/demo.ahk', 27 | ideKey: '', 28 | language: 'AutoHotkey', 29 | parent: '', 30 | protocolVersion: '1.0', 31 | session: '', 32 | thread: '7208', 33 | }); 34 | }); 35 | this.serverSocket.on('data', () => { 36 | const packet = packets.shift(); 37 | if (typeof packet === 'undefined') { 38 | done(); 39 | return; 40 | } 41 | this.socket.write(Buffer.from(String(packet))); 42 | }); 43 | 44 | const packet = packets.shift(); 45 | this.socket.write(Buffer.from(String(packet))); 46 | }); 47 | teardown(function() { 48 | this.socket.end(); 49 | this.session.close(); 50 | this.server.close(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; 3 | import { splitVariablePath } from '../src/util/util'; 4 | 5 | suite('splitVariablePath', () => { 6 | test('v1', () => { 7 | const ahkVersion = new AhkVersion('1.1.00'); 8 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a'), [ 'a' ]); 9 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a.'), [ 'a', '' ]); 10 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a.b'), [ 'a', 'b' ]); 11 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a.b["c"]'), [ 'a', 'b', '["c"]' ]); 12 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a.b["c"].d'), [ 'a', 'b', '["c"]', 'd' ]); 13 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a["b.B"]["c"].d'), [ 'a', '["b.B"]', '["c"]', 'd' ]); 14 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a["""b.B"""]["c"].d'), [ 'a', '["""b.B"""]', '["c"]', 'd' ]); 15 | 16 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a b.test'), [ 'a b', 'test' ]); 17 | }); 18 | test('v2', () => { 19 | const ahkVersion = new AhkVersion('2.0.00'); 20 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a'), [ 'a' ]); 21 | assert.deepStrictEqual(splitVariablePath(ahkVersion, 'a.b'), [ 'a', 'b' ]); 22 | assert.deepStrictEqual(splitVariablePath(ahkVersion, `a.b['c']`), [ 'a', 'b', `['c']` ]); 23 | assert.deepStrictEqual(splitVariablePath(ahkVersion, `a.b['c'].d`), [ 'a', 'b', `['c']`, 'd' ]); 24 | assert.deepStrictEqual(splitVariablePath(ahkVersion, `a['\`'b.B\`'']['c'].d`), [ 'a', `['\`'b.B\`'']`, `['c']`, 'd' ]); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "commonjs", 5 | "lib": [ 6 | "ESNext", 7 | "DOM" 8 | ], 9 | "types": [ 10 | "node", 11 | "mocha" 12 | ], 13 | 14 | "outDir": "build", 15 | "sourceMap": true, 16 | "strict": true, 17 | "declaration": true, 18 | "skipLibCheck": true, 19 | "inlineSourceMap": false, 20 | "noImplicitAny": false, 21 | "noImplicitThis": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": false, 24 | "preserveConstEnums": true, 25 | "esModuleInterop": true, 26 | "removeComments": false, 27 | "resolveJsonModule": true, 28 | "newLine": "crlf" 29 | }, 30 | "include": [ 31 | "src/**/*", 32 | "test/**/*", 33 | "gulpfile.ts" 34 | ] 35 | } 36 | --------------------------------------------------------------------------------