diff --git a/CHANGELOG.md b/CHANGELOG.md index f2249396..d01e94f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Also want to check the development status, check the [commit history](https://gi * [#224](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/224) Add `{callstack.trace}` to the MetaVariable * [#257](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/257) Add Exception Breakpoint * [#264](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/264) Add `vscode-autohotkey-debug.commands.runToEndOfFunction` vscode command +* [#266](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/266) Add `setHiddenBreakpoints` attribute in launch.json * [#271](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/271) Add `vscode-autohotkey-debug.variables.pinnedFile` [command variable](https://code.visualstudio.com/docs/editor/variables-reference#_command-variables) * [#271](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/271) Add `vscode-autohotkey-debug.variables.leftmostFile` [command variable](https://code.visualstudio.com/docs/editor/variables-reference#_command-variables) * [#271](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/271) Add `vscode-autohotkey-debug.variables.rightmostFile` [command variable](https://code.visualstudio.com/docs/editor/variables-reference#_command-variables) diff --git a/README.md b/README.md index 927dc2a2..f9b56ecf 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This extension is a debugger adapter for [VSCode](https://code.visualstudio.com/ * Added: [#224](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/224) Add `{callstack.trace}` to the MetaVariable * Added: [#257](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/257) Add Exception Breakpoint * Added: [#264](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/264) Add `vscode-autohotkey-debug.commands.runToEndOfFunction` vscode command + * Added: [#266](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/266) Add `setHiddenBreakpoints` attribute in launch.json * Added: [#271](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/271) Add `vscode-autohotkey-debug.variables.pinnedFile` [command variable](https://code.visualstudio.com/docs/editor/variables-reference#_command-variables) * Added: [#271](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/271) Add `vscode-autohotkey-debug.variables.leftmostFile` [command variable](https://code.visualstudio.com/docs/editor/variables-reference#_command-variables) * Added: [#271](https://github.com/zero-plusplus/vscode-autohotkey-debug/issues/271) Add `vscode-autohotkey-debug.variables.rightmostFile` [command variable](https://code.visualstudio.com/docs/editor/variables-reference#_command-variables) diff --git a/demo/.vscode/launch.json b/demo/.vscode/launch.json index 40be9f44..8bcc53df 100644 --- a/demo/.vscode/launch.json +++ b/demo/.vscode/launch.json @@ -26,7 +26,6 @@ }, "useExceptionBreakpoint": true, "useAnnounce": "detail", - "useFunctionBreakpoint": true, "variableCategories": "recommend" }, { @@ -146,6 +145,72 @@ ], "useAnnounce": "detail", }, + { + "name": "setHiddenBreakpoints Test", + "type": "autohotkey", + "request": "launch", + "program": "${file}", + "useExceptionBreakpoint": true, + "setHiddenBreakpoints": [ + { + "label": "Stop On Entry", + "breakpoints": [ + { + "target": "${file}", + "line": 1, + } + ], + }, + { + "label": "Stop On Exit", + "breakpoints": [ + { + "target": "${file}", + "line": -1, + } + ], + }, + { + "label": "Stop On Entry/Exit", + "breakpoints": [ + { + "target": "${file}", + "line": [ 1, -1 ], + } + ], + }, + { + "label": "Function Trace", + "breakpoints": [ + { + "target": "*", + "line": 1, + "log": "{A_ThisFunc} start" + }, + { + "target": "*", + "line": -1, + "log": "{A_ThisFunc} end", + }, + ] + }, + { + "label": "Global Variable Trace", + "breakpoints": [ + { + "target": "${workspaceFolder}/*.*", + "line": { + "pattern": "^\\s*(global)\\s+(?[a-zA-Z0-9@#$]+)\\s*(?=:=)", + "select": "all", + "ignoreCase": true, + "offset": 1, + }, + "log": "{GetMeta(\"$1\")} {GetMeta(\"$var\")} := \"{GetVar(GetMeta(\"$var\"))}\"", + }, + ] + } + ], + }, { "name": "AutoHotkey Debug (UIA)", "type": "autohotkey", diff --git a/demo/demo.ahk b/demo/demo.ahk index 2fef37b5..c03135e1 100644 --- a/demo/demo.ahk +++ b/demo/demo.ahk @@ -1,5 +1,6 @@ #SingleInstance Force #Warn All, StdOut + globalVar := "Global" global SuperGlobalVar := "SuperGlobal" @@ -50,6 +51,7 @@ demo() circular.circular := circular instance := new Clazz() + property := instance.property instance.property := "overwrite" instance.method() } @@ -81,4 +83,4 @@ class Clazz extends ClazzBase class ClazzBase { baseField := "baseField" -} \ No newline at end of file +} diff --git a/demo/demo.ahk2 b/demo/demo.ahk2 index 3589ab8e..8538c851 100644 --- a/demo/demo.ahk2 +++ b/demo/demo.ahk2 @@ -1,8 +1,7 @@ -#Warn All, StdOut #SingleInstance Force -; #Include -#Include "%A_LineFile%/../lib/Util.ahk" +#Warn All, StdOut +#Include "%A_LineFile%/../lib/Util.ahk" globalVar := "Global" global SuperGlobalVar := "SuperGlobal" @@ -63,6 +62,7 @@ demo() circular.circular := circular instance := Clazz() + property := instance.property instance.property := "overwrite" instance.method() } @@ -106,4 +106,4 @@ class Clazz extends ClazzBase class ClazzBase { baseField := "baseField" -} \ No newline at end of file +} diff --git a/package.json b/package.json index a25930cb..278577f8 100644 --- a/package.json +++ b/package.json @@ -515,38 +515,6 @@ "description": "If set `true`, exception breakpoint can be enabled. But this feature requires that the runtime supports exception breakpoint.", "default": true }, - "useFunctionBreakpoint": { - "type": [ - "boolean", - "array" - ], - "items": { - "type": [ - "string", - "object" - ], - "properties": { - "name": { - "type": "string", - "description": "A name or wildcard of function to set breakpoint." - }, - "condition": { - "type": "string", - "description": "A condition of a function breakpoint." - }, - "hitCondition": { - "type": "string", - "description": "A hit condition of a function breakpoint." - }, - "logPoint": { - "type": "string", - "description": "A log point of a function breakpoint." - } - } - }, - "description": "If set `true`, function breakpoint can be enabled.", - "default": true - }, "skipFunctions": { "type": "array", "items": { @@ -696,6 +664,204 @@ } } }, + "setHiddenBreakpoints": { + "type": "array", + "items": { + "type": "object", + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "description": "A label for `breakpoints` to be displayed in the Exception Breakpoints UI" + }, + "breakpoints": { + "type": "array", + "description": "Hidden breakpoints to associate with `label`.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, + "description": "Target for setting the hidden breakpoint. Specify a glob path or function/method/property name with wildcard. If it ends in `()` such as `func()`, only functions/methods are targeted, if it ends in `[]`, properties are targeted. If specified in an array, hidden breakpoints are set on all targets that match the condition." + }, + "line": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "pattern": { + "type": "string", + "description": "Regular expression to be applied to each line." + }, + "ignoreCase": { + "type": "boolean", + "description": "Make the `pattern` case-insensitive when matching." + }, + "select": { + "type": [ + "string", + "number", + "array" + ], + "items": { + "type": "number" + }, + "enum": [ + "first", + "last", + "all" + ], + "description": "Controls whether some or all breakpoints are set when multiple lines are matched. When omitted, \"all\" is specified." + }, + "offset": { + "type": "number", + "description": "Offset from matched line." + } + } + } + ], + "description": "A line number (1-base) to set the hidden breakpoint. Or a matcher to identify the line number. If a negative line number is given, it is treated as an offset from the end. By specifying an array, it is possible to set a hidden breakpoint on multiple lines." + }, + "condition": { + "type": "string", + "description": "A condition to be set for the hidden breakpoint." + }, + "hitCondition": { + "type": "string", + "description": "A hit condition to be set for the hidden breakpoint." + }, + "log": { + "type": "string", + "description": "A log message to be set for the hidden breakpoint." + }, + "action": { + "type": "string", + "enum": [ + "ClearConsole" + ], + "description": "An action to be executed when a hidden breakpoint is reached." + }, + "break": { + "type": "boolean", + "description": "If `false` is specified, no break is made. If omitted, `false` is set if `log` or `action` is specified, otherwise `true`.", + "default": true + } + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, + "description": "Target for setting the hidden breakpoint. Specify a glob path or function/method/property name with wildcard. If it ends in `()` such as `func()`, only functions/methods are targeted, if it ends in `[]`, properties are targeted. If specified in an array, hidden breakpoints are set on all targets that match the condition." + }, + "line": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "pattern": { + "type": "string", + "description": "Regular expression to be applied to each line." + }, + "ignoreCase": { + "type": "boolean", + "description": "Make the `pattern` case-insensitive when matching." + }, + "select": { + "type": [ + "string", + "number", + "array" + ], + "items": { + "type": "number" + }, + "enum": [ + "first", + "last", + "all" + ], + "description": "Controls whether some or all breakpoints are set when multiple lines are matched. When omitted, \"all\" is specified." + }, + "offset": { + "type": "number", + "description": "Offset from matched line." + } + } + } + ], + "description": "A line number (1-base) to set the hidden breakpoint. Or a matcher to identify the line number. If a negative line number is given, it is treated as an offset from the end. By specifying an array, it is possible to set a hidden breakpoint on multiple lines." + }, + "condition": { + "type": "string", + "description": "A condition to be set for the hidden breakpoint." + }, + "hitCondition": { + "type": "string", + "description": "A hit condition to be set for the hidden breakpoint." + }, + "log": { + "type": "string", + "description": "A log message to be set for the hidden breakpoint." + }, + "action": { + "type": "string", + "enum": [ + "ClearConsole" + ], + "description": "An action to be executed when a hidden breakpoint is reached." + }, + "break": { + "type": "boolean", + "description": "If `false` is specified, no break is made. If omitted, `false` is set if `log` or `action` is specified, otherwise `true`.", + "default": true + } + } + } + ] + } + }, "trace": { "type": "boolean", "description": "No changes are required. This is a settings for developers to use to find bugs. Enable / disable display trace informaiton for debugger adapter.", @@ -884,38 +1050,6 @@ "description": "If set `true`, exception breakpoint can be enabled. But this feature requires that the runtime supports exception breakpoint.", "default": true }, - "useFunctionBreakpoint": { - "type": [ - "boolean", - "array" - ], - "items": { - "type": [ - "string", - "object" - ], - "properties": { - "name": { - "type": "string", - "description": "A name or wildcard of function to set breakpoint." - }, - "condition": { - "type": "string", - "description": "A condition of a function breakpoint." - }, - "hitCondition": { - "type": "string", - "description": "A hit condition of a function breakpoint." - }, - "logPoint": { - "type": "string", - "description": "A log point of a function breakpoint." - } - } - }, - "description": "If set `true`, function breakpoint can be enabled.", - "default": true - }, "skipFunctions": { "type": "array", "items": { @@ -1065,6 +1199,204 @@ } } }, + "setHiddenBreakpoints": { + "type": "array", + "items": { + "type": "object", + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "description": "A label for `breakpoints` to be displayed in the Exception Breakpoints UI" + }, + "breakpoints": { + "type": "array", + "description": "Hidden breakpoints to associate with `label`.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, + "description": "Target for setting the hidden breakpoint. Specify a glob path or function/method/property name with wildcard. If it ends in `()` such as `func()`, only functions/methods are targeted, if it ends in `[]`, properties are targeted. If specified in an array, hidden breakpoints are set on all targets that match the condition." + }, + "line": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "pattern": { + "type": "string", + "description": "Regular expression to be applied to each line." + }, + "ignoreCase": { + "type": "boolean", + "description": "Make the `pattern` case-insensitive when matching." + }, + "select": { + "type": [ + "string", + "number", + "array" + ], + "items": { + "type": "number" + }, + "enum": [ + "first", + "last", + "all" + ], + "description": "Controls whether some or all breakpoints are set when multiple lines are matched. When omitted, \"all\" is specified." + }, + "offset": { + "type": "number", + "description": "Offset from matched line." + } + } + } + ], + "description": "A line number (1-base) to set the hidden breakpoint. Or a matcher to identify the line number. If a negative line number is given, it is treated as an offset from the end. By specifying an array, it is possible to set a hidden breakpoint on multiple lines." + }, + "condition": { + "type": "string", + "description": "A condition to be set for the hidden breakpoint." + }, + "hitCondition": { + "type": "string", + "description": "A hit condition to be set for the hidden breakpoint." + }, + "log": { + "type": "string", + "description": "A log message to be set for the hidden breakpoint." + }, + "action": { + "type": "string", + "enum": [ + "ClearConsole" + ], + "description": "An action to be executed when a hidden breakpoint is reached." + }, + "break": { + "type": "boolean", + "description": "If `false` is specified, no break is made. If omitted, `false` is set if `log` or `action` is specified, otherwise `true`.", + "default": true + } + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, + "description": "Target for setting the hidden breakpoint. Specify a glob path or function/method/property name with wildcard. If it ends in `()` such as `func()`, only functions/methods are targeted, if it ends in `[]`, properties are targeted. If specified in an array, hidden breakpoints are set on all targets that match the condition." + }, + "line": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "pattern": { + "type": "string", + "description": "Regular expression to be applied to each line." + }, + "ignoreCase": { + "type": "boolean", + "description": "Make the `pattern` case-insensitive when matching." + }, + "select": { + "type": [ + "string", + "number", + "array" + ], + "items": { + "type": "number" + }, + "enum": [ + "first", + "last", + "all" + ], + "description": "Controls whether some or all breakpoints are set when multiple lines are matched. When omitted, \"all\" is specified." + }, + "offset": { + "type": "number", + "description": "Offset from matched line." + } + } + } + ], + "description": "A line number (1-base) to set the hidden breakpoint. Or a matcher to identify the line number. If a negative line number is given, it is treated as an offset from the end. By specifying an array, it is possible to set a hidden breakpoint on multiple lines." + }, + "condition": { + "type": "string", + "description": "A condition to be set for the hidden breakpoint." + }, + "hitCondition": { + "type": "string", + "description": "A hit condition to be set for the hidden breakpoint." + }, + "log": { + "type": "string", + "description": "A log message to be set for the hidden breakpoint." + }, + "action": { + "type": "string", + "enum": [ + "ClearConsole" + ], + "description": "An action to be executed when a hidden breakpoint is reached." + }, + "break": { + "type": "boolean", + "description": "If `false` is specified, no break is made. If omitted, `false` is set if `log` or `action` is specified, otherwise `true`.", + "default": true + } + } + } + ] + } + }, "trace": { "type": "boolean", "description": "No changes are required. This is a settings for developers to use to find bugs. Enable / disable display trace informaiton for debugger adapter.", diff --git a/src/ahkDebug.ts b/src/ahkDebug.ts index 66e2138d..5c2dd88e 100644 --- a/src/ahkDebug.ts +++ b/src/ahkDebug.ts @@ -23,6 +23,7 @@ import LazyPromise from 'lazy-promise'; import { ImplicitLibraryPathExtractor, IncludePathExtractor } from '@zero-plusplus/autohotkey-utilities'; import { Breakpoint, + BreakpointAction, BreakpointAdvancedData, BreakpointManager, LineBreakpoints, @@ -33,7 +34,7 @@ import { TraceLogger } from './util/TraceLogger'; import { completionItemProvider } from './CompletionItemProvider'; import * as dbgp from './dbgpSession'; import { AutoHotkeyLauncher, AutoHotkeyProcess } from './util/AutoHotkeyLuncher'; -import { now, timeoutPromise, toFileUri } from './util/util'; +import { now, readFileCache, readFileCacheSync, timeoutPromise, toFileUri } from './util/util'; import matcher from 'matcher'; import { Categories, Category, MetaVariable, MetaVariableValue, MetaVariableValueMap, Scope, StackFrames, Variable, VariableManager, formatProperty } from './util/VariableManager'; import { AhkConfigurationProvider, CategoryData } from './extension'; @@ -41,9 +42,39 @@ import { version as debuggerAdapterVersion } from '../package.json'; import { SymbolFinder } from './util/SymbolFinder'; import { ExpressionEvaluator, ParseError, toJavaScriptBoolean, toType } from './util/evaluator/ExpressionEvaluator'; import { enableRunToEndOfFunction, setEnableRunToEndOfFunction } from './commands'; +import { CaseInsensitiveMap } from './util/CaseInsensitiveMap'; export type AnnounceLevel = boolean | 'error' | 'detail'; export type FunctionBreakPointAdvancedData = { name: string; condition?: string; hitCondition?: string; logPoint?: string }; + +export type MatchSelector = undefined | 'first' | 'last' | 'all' | number | number[]; +export type LineMatcher = + | number + | number[] + | RegExLineMatcher; +export interface RegExLineMatcher { + pattern: string; + ignoreCase?: boolean; + select?: MatchSelector; + offset?: number; +} +export type LineMatchResult = { line: number; match?: RegExpMatchArray }; +export type HiddenBreakpointActionName = 'ClearConsole'; +export interface HiddenBreakpoint { + target: string | string[]; + line: LineMatcher; + condition?: string; + hitCondition?: string; + log?: string; + break: boolean; + action?: HiddenBreakpointActionName; +} +export interface HiddenBreakpointWithUI { + id: string; + label: string; + breakpoints: HiddenBreakpoint[]; +} + export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, DebugProtocol.AttachRequestArguments { name: string; program: string; @@ -79,12 +110,12 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum scanImplicitLibrary: boolean; }; useExceptionBreakpoint: boolean; - useFunctionBreakpoint: boolean | Array; openFileOnExit: string; trace: boolean; skipFunctions?: string[]; skipFiles?: string[]; variableCategories?: CategoryData[]; + setHiddenBreakpoints?: Array; // The following is not a configuration, but is set to pass data to the debug adapter. cancelReason?: string; extensionContext: vscode.ExtensionContext; @@ -110,7 +141,10 @@ export class AhkDebugSession extends LoggingDebugSession { private isTerminateRequested = false; private readonly configProvider: AhkConfigurationProvider; private symbolFinder?: sym.SymbolFinder; - private callableSymbols: sym.NamedNodeBase[] | undefined; + private symbols?: sym.NamedNode[]; + private callableSymbols?: sym.NamedNode[]; + private hiddenBreakpoints?: HiddenBreakpoint[]; + private hiddenBreakpointsWithUI?: HiddenBreakpointWithUI[]; private exceptionArgs?: DebugProtocol.SetExceptionBreakpointsArguments; private server?: net.Server; private ahkProcess?: AutoHotkeyProcess; @@ -155,36 +189,58 @@ export class AhkDebugSession extends LoggingDebugSession { supportsEvaluateForHovers: true, supportsSetExpression: true, supportsHitConditionalBreakpoints: true, + supportsFunctionBreakpoints: true, supportsLoadedSourcesRequest: true, supportsLogPoints: true, supportsSetVariable: true, supportTerminateDebuggee: true, }; - const config = this.configProvider.config!; - if (config.useExceptionBreakpoint) { + const config = this.configProvider.config; + if (config?.setHiddenBreakpoints) { + this.hiddenBreakpoints = config.setHiddenBreakpoints.filter((hiddenBreakpoint) => !('label' in hiddenBreakpoint)) as HiddenBreakpoint[]; + this.hiddenBreakpointsWithUI = config.setHiddenBreakpoints.filter((hiddenBreakpoint) => 'label' in hiddenBreakpoint) as HiddenBreakpointWithUI[]; + + response.body.exceptionBreakpointFilters = Array.isArray(response.body.exceptionBreakpointFilters) + ? response.body.exceptionBreakpointFilters + : []; + response.body.exceptionBreakpointFilters.push(...this.hiddenBreakpointsWithUI.map((hiddenBreakpoint, i): DebugProtocol.ExceptionBreakpointsFilter => { + hiddenBreakpoint.id = `hidden-breakpoint-${i}`; + return { + label: hiddenBreakpoint.label, + filter: hiddenBreakpoint.id, + supportsCondition: false, + default: false, + }; + })); + } + + if (config?.useExceptionBreakpoint) { response.body.supportsExceptionOptions = true; response.body.supportsExceptionFilterOptions = true; response.body.supportsExceptionInfoRequest = true; - response.body.exceptionBreakpointFilters = [ + + response.body.exceptionBreakpointFilters = Array.isArray(response.body.exceptionBreakpointFilters) + ? response.body.exceptionBreakpointFilters + : []; + response.body.exceptionBreakpointFilters.push(...[ { filter: 'caught-exceptions', label: 'Caught Exceptions', conditionDescription: '', + description: '', supportsCondition: true, default: false, }, { filter: 'uncaught-exceptions', - label: 'Uncaught Breakpoint', + label: 'Uncaught Exceptions', conditionDescription: '', + description: '', supportsCondition: false, default: false, }, - ]; - } - if (config.useFunctionBreakpoint) { - response.body.supportsFunctionBreakpoints = true; + ] as DebugProtocol.ExceptionBreakpointsFilter[]); } this.sendResponse(response); @@ -390,8 +446,19 @@ export class AhkDebugSession extends LoggingDebugSession { protected async setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments, request?: DebugProtocol.Request | undefined): Promise { this.exceptionArgs = args; - const state = (this.exceptionArgs.filterOptions ?? []).some((filter) => filter.filterId.endsWith('exceptions')); - await this.session!.sendExceptionBreakpointCommand(state); + const exceptionEnabled = (this.exceptionArgs.filterOptions ?? []).some((filter) => filter.filterId.endsWith('exceptions')); + await this.session!.sendExceptionBreakpointCommand(exceptionEnabled); + + if (this.hiddenBreakpointsWithUI) { + this.unregisterHiddenBreakpointsWithUI(); + + for await (const filterOption of this.exceptionArgs.filterOptions ?? []) { + const hiddenBreakpointWithUI = this.hiddenBreakpointsWithUI.find((breakpoint) => filterOption.filterId === breakpoint.id); + if (hiddenBreakpointWithUI) { + await this.registerHiddenBreakpointsWithUI(hiddenBreakpointWithUI); + } + } + } this.sendResponse(response); } protected async exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, args: DebugProtocol.ExceptionInfoArguments, request?: DebugProtocol.Request): Promise { @@ -422,20 +489,11 @@ export class AhkDebugSession extends LoggingDebugSession { this.sendResponse(response); } protected async setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments, request?: DebugProtocol.Request | undefined): Promise { - const breakpoints: Array = [ ...args.breakpoints ]; - if (!this.callableSymbols) { - if (typeof this.config.useFunctionBreakpoint === 'object') { - breakpoints.push(...this.config.useFunctionBreakpoint.map((breakpoint) => { - return typeof breakpoint === 'string' ? { name: breakpoint } : breakpoint; - })); - } - this.callableSymbols = this.initCallableSymbols(); - } + this.initSymbols(); - for await (const breakpoint of breakpoints) { - const isMatch = wildcard(breakpoint.name, { separator: '.' }); - const symbols = this.callableSymbols.filter((symbol) => isMatch(sym.getFullName(symbol))); - for await (const symbol of symbols) { + for await (const breakpoint of args.breakpoints) { + const callableSymbols = this.findCallableSymbols(breakpoint.name); + for await (const symbol of callableSymbols) { try { const existsFunctionBreakpoint = this.registeredFunctionBreakpoints.find((breakpoint) => { return breakpoint.group === 'function-breakpoint' @@ -459,9 +517,6 @@ export class AhkDebugSession extends LoggingDebugSession { unverifiedLine: line, unverifiedColumn: 1, } as BreakpointAdvancedData; - if ('logPoint' in breakpoint && breakpoint.logPoint !== '') { - advancedData.logMessage = breakpoint.logPoint; - } const registeredBreakpoint = await this.breakpointManager!.registerBreakpoint(fileUri, line, advancedData); this.registeredFunctionBreakpoints.push(registeredBreakpoint); @@ -483,6 +538,7 @@ export class AhkDebugSession extends LoggingDebugSession { await this.session!.sendFeatureSetCommand('max_children', this.config.maxChildren); await this.registerDebugDirective(); + await this.registerHiddenBreakpoints(); const result = this.config.stopOnEntry ? await this.session!.sendContinuationCommand('step_into') @@ -543,11 +599,12 @@ export class AhkDebugSession extends LoggingDebugSession { let runToEndOfFunctionBreakpoint: Breakpoint | undefined; if (enableRunToEndOfFunction) { - this.callableSymbols = this.initCallableSymbols(); + this.initSymbols(); + const filePath = this.currentStackFrames?.[0].source.path; const funcName = this.currentStackFrames?.[0].name; if (filePath && funcName?.includes('()')) { - const symbol = this.callableSymbols.find((symbol) => funcName.match(new RegExp(`^${symbol.fullname}()`, 'iu'))); + const symbol = this.callableSymbols?.find((symbol) => funcName.match(new RegExp(`^${symbol.fullname}()$`, 'iu'))); if (symbol?.location.endIndex) { const fileUri = toFileUri(filePath); const line = sym.getLine(symbol, symbol.location.endIndex); @@ -950,7 +1007,6 @@ export class AhkDebugSession extends LoggingDebugSession { this.sendResponse(response); return; } - const loadedScriptPathList = await this.getAllLoadedSourcePath(); const sources: DebugProtocol.Source[] = []; @@ -974,14 +1030,13 @@ export class AhkDebugSession extends LoggingDebugSession { response.body = { sources }; this.sendResponse(response); } - private initCallableSymbols(): sym.NamedNodeBase[] { - if (this.callableSymbols) { - return this.callableSymbols; - } - this.callableSymbols = this.symbolFinder! - .find(this.config.program) - .filter((node) => [ 'function', 'getter', 'setter' ].includes(node.type)) as sym.NamedNodeBase[]; - return this.callableSymbols; + private initSymbols(): void { + if (this.symbols) { + return; + } + + this.symbols = this.symbolFinder!.find(this.config.program).filter((symbol) => 'name' in symbol) as sym.NamedNode[]; + this.callableSymbols = this.symbols.filter((node) => [ 'function', 'getter', 'setter' ].includes(node.type)); } private async getAllLoadedSourcePath(): Promise { if (0 < this.loadedSources.length) { @@ -1008,6 +1063,12 @@ export class AhkDebugSession extends LoggingDebugSession { catch (e: unknown) { } } + private async clearDebugConsole(): Promise { + // There is a lag between the execution of a command and the console being cleared. This lag can be eliminated by executing the command multiple times. + await vscode.commands.executeCommand('workbench.debug.panel.action.clearReplAction'); + await vscode.commands.executeCommand('workbench.debug.panel.action.clearReplAction'); + await vscode.commands.executeCommand('workbench.debug.panel.action.clearReplAction'); + } private async registerDebugDirective(): Promise { if (!this.config.useDebugDirective) { return; @@ -1096,12 +1157,7 @@ export class AhkDebugSession extends LoggingDebugSession { hitCondition, logMessage, hidden: true, - action: async() => { - // There is a lag between the execution of a command and the console being cleared. This lag can be eliminated by executing the command multiple times. - await vscode.commands.executeCommand('workbench.debug.panel.action.clearReplAction'); - await vscode.commands.executeCommand('workbench.debug.panel.action.clearReplAction'); - await vscode.commands.executeCommand('workbench.debug.panel.action.clearReplAction'); - }, + action: this.createBreakpointAction('ClearConsole'), } as BreakpointAdvancedData; await this.breakpointManager!.registerBreakpoint(fileUri, line, advancedData); } @@ -1121,6 +1177,224 @@ export class AhkDebugSession extends LoggingDebugSession { // const DEBUG_s = DEBUG_ns / 1e9; // this.printLogMessage(`elapsedTime: ${DEBUG_s}s`); } + private createBreakpointAction(action?: HiddenBreakpointActionName): BreakpointAction | undefined { + if (typeof action === 'undefined') { + return undefined; + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (action === 'ClearConsole') { + return async() => { + await this.clearDebugConsole(); + }; + } + return undefined; + } + private async matchLineNumbers(filePathOrNode: string | sym.NamedNode, lineMatcher: LineMatcher): Promise { + if (typeof lineMatcher === 'number') { + if (lineMatcher === 0) { + return [ { line: 1 } ]; + } + if (0 < lineMatcher) { + if (typeof filePathOrNode === 'string') { + return [ { line: lineMatcher } ]; + } + + const node = filePathOrNode; + return [ { line: sym.getLine(node, node.location.startIndex) + (lineMatcher - 1) } ]; + } + + if (typeof filePathOrNode === 'string') { + const source = await readFileCache(filePathOrNode); + const lineCount = source.split(/\r\n|\n/u).length; + return [ { line: lineCount - Math.abs(lineMatcher + 1) } ]; + } + + const node = filePathOrNode; + return [ { line: sym.getLine(node, node.location.endIndex) - Math.abs(lineMatcher + 1) } ]; + } + else if (Array.isArray(lineMatcher)) { + return (await Promise.all(lineMatcher.map(async(line) => this.matchLineNumbers(filePathOrNode, line)))).flat(); + } + + if ('pattern' in lineMatcher) { + return this.regexMatchLines(filePathOrNode, lineMatcher); + } + return []; + } + private async regexMatchLines(filePathOrNode: string | sym.NamedNode, matcher: RegExLineMatcher): Promise { + const source = typeof filePathOrNode === 'string' + ? await readFileCache(filePathOrNode) + : sym.getText(filePathOrNode); + const startLine = typeof filePathOrNode === 'string' ? 0 : sym.getLine(filePathOrNode, filePathOrNode.location.startIndex); + + const lines = source.split(/\r\n|\n/u); + const regexp = new RegExp(matcher.pattern, matcher.ignoreCase ? 'ui' : 'u'); + const offset = matcher.offset ? matcher.offset : 0; + const matchedLines = lines + .flatMap((line, i): LineMatchResult[] => { + const match = line.match(regexp); + if (!match) { + return []; + } + return [ { line: (startLine + (i + 1)) + offset, match } ]; + }); + return this.selectLineMatchResults(matchedLines, matcher.select); + } + private selectLineMatchResults(results: LineMatchResult[], selector: MatchSelector): LineMatchResult[] { + return results.filter((result, i) => { + if (typeof selector === 'undefined') { + return true; + } + if (selector === 'first' && i === 0) { + return true; + } + if (selector === 'last' && (results.length - 1) === i) { + return true; + } + if (selector === 'all') { + return true; + } + + const i_1base = i + 1; + if (typeof selector === 'number' && selector === i_1base) { + return true; + } + else if (Array.isArray(selector) && selector.includes(i_1base)) { + return true; + } + return false; + }); + } + private findCallableSymbols(matcher: string): sym.NamedNode[] { + this.initSymbols(); + + const regex = /(\[\]|\(\))$/u; + const target = matcher.replace(regex, ''); + const targetKind = matcher.match(regex)?.[0]; + + const isMatch = wildcard(target, { separator: '.', flags: 'i' }); + return (this.callableSymbols ?? []).filter((symbol, i, symbols) => { + const matchResult = isMatch(symbol.fullname); + if (targetKind === '()') { + return matchResult && symbol.type === 'function'; + } + if (targetKind === '[]') { + return matchResult && (symbol.type === 'property' || symbol.type === 'getter' || symbol.type === 'setter'); + } + if (!matchResult && symbol.type === 'getter') { + const isMatch = wildcard(`${target}.get`, { separator: '.', flags: 'i' }); + return symbols.some((symbol) => { + return isMatch(symbol.fullname); + }); + } + if (!matchResult && symbol.type === 'setter') { + const isMatch = wildcard(`${target}.set`, { separator: '.', flags: 'i' }); + return symbols.some((symbol) => { + return isMatch(symbol.fullname); + }); + } + return matchResult; + }); + } + private async registerHiddenBreakpoint(breakpointData: HiddenBreakpoint, groupName: string): Promise { + const createAdditionalMetaVariables = (lineMatchResult: LineMatchResult): CaseInsensitiveMap | undefined => { + if (!lineMatchResult.match) { + return undefined; + } + + const additionalMetaVariables = new CaseInsensitiveMap(); + lineMatchResult.match.forEach((match, i) => { + additionalMetaVariables.set(`$${i}`, match); + }); + Object.entries(lineMatchResult.match.groups ?? []).forEach(([ key, value ], i) => { + additionalMetaVariables.set(`$${key}`, value); + }); + return additionalMetaVariables; + }; + + const shouldBreak = typeof breakpointData.break === 'undefined' + ? !(breakpointData.log ?? breakpointData.action) + : breakpointData.break; + const logMessage = breakpointData.log ? `${breakpointData.log}\n` : undefined; + const action = this.createBreakpointAction(breakpointData.action); + + const targets = Array.isArray(breakpointData.target) ? breakpointData.target : [ breakpointData.target ]; + for await (const target of targets) { + const fileMode = target.includes('/') || target.includes('\\'); + if (fileMode) { + const loadedSources = await this.getAllLoadedSourcePath(); + const replaceSlash = (filePath: string): string => path.resolve(filePath).replaceAll('\\', '/'); + + const isMatch = wildcard(targets.map((target) => replaceSlash(target)), { flags: 'i' }); + const targetFilePathList = loadedSources.filter((source) => { + return isMatch(replaceSlash(source)); + }); + + for await (const filePath of targetFilePathList) { + const lineMatchResults = await this.matchLineNumbers(filePath, breakpointData.line); + + for await (const lineMatchResult of lineMatchResults) { + await this.breakpointManager!.registerBreakpoint(toFileUri(filePath), lineMatchResult.line, { + condition: breakpointData.condition, + hitCondition: breakpointData.hitCondition, + shouldBreak, + hidden: true, + logMessage, + additionalMetaVariables: createAdditionalMetaVariables(lineMatchResult), + group: groupName, + action, + }); + } + } + return; + } + + const callableSymbols = this.findCallableSymbols(target); + for await (const callableSymbol of callableSymbols) { + const lineMatchResults = await this.matchLineNumbers(callableSymbol, breakpointData.line); + + for await (const lineMatchResult of lineMatchResults) { + await this.breakpointManager!.registerBreakpoint(toFileUri(callableSymbol.location.sourceFile), lineMatchResult.line, { + condition: breakpointData.condition, + hitCondition: breakpointData.hitCondition, + shouldBreak, + hidden: true, + logMessage, + additionalMetaVariables: createAdditionalMetaVariables(lineMatchResult), + group: groupName, + action, + }); + } + } + } + } + private async registerHiddenBreakpoints(): Promise { + if (!this.hiddenBreakpoints) { + return; + } + + for await (const breakpointData of this.hiddenBreakpoints) { + await this.registerHiddenBreakpoint(breakpointData, 'hidden-breakpoint'); + } + } + private async registerHiddenBreakpointsWithUI(hiddenBreakpoint: HiddenBreakpointWithUI): Promise { + for await (const breakpoint of hiddenBreakpoint.breakpoints) { + await this.registerHiddenBreakpoint(breakpoint, hiddenBreakpoint.id); + } + } + private async unregisterHiddenBreakpointWithUI(hiddenBreakpoint: HiddenBreakpointWithUI): Promise { + return this.breakpointManager!.unregisterBreakpointGroup(hiddenBreakpoint.id); + } + private async unregisterHiddenBreakpointsWithUI(): Promise { + if (!this.hiddenBreakpointsWithUI) { + return; + } + + for await (const hiddenBreakpointWithUI of this.hiddenBreakpointsWithUI) { + await this.unregisterHiddenBreakpointWithUI(hiddenBreakpointWithUI); + } + } private fixPathOfRuntimeError(errorMessage: string): string { if (-1 < errorMessage.search(/--->\t\d+:/gmu)) { const line = parseInt(errorMessage.match(/--->\t(?\d+):/u)!.groups!.line, 10); @@ -1451,7 +1725,17 @@ export class AhkDebugSession extends LoggingDebugSession { await breakpoint.action(); } if ((breakpoint.logMessage || breakpoint.logGroup) && await this.evaluateCondition(breakpoint)) { + const tempMetaVariableNames: string[] = []; + if (breakpoint.additionalMetaVariables) { + breakpoint.additionalMetaVariables.forEach((value, key) => { + tempMetaVariableNames.push(key); + this.currentMetaVariableMap.set(key, value); + }); + } + await this.printLogMessage(breakpoint, 'stdout'); + + tempMetaVariableNames.forEach((name) => this.currentMetaVariableMap.delete(name)); } } } @@ -1871,9 +2155,7 @@ export class AhkDebugSession extends LoggingDebugSession { this.evaluator = new ExpressionEvaluator(this.session, this.currentMetaVariableMap); completionItemProvider.useIntelliSenseInDebugging = this.config.useIntelliSenseInDebugging; completionItemProvider.evaluator = this.evaluator; - if (this.config.useFunctionBreakpoint) { - this.symbolFinder = new SymbolFinder(this.session.ahkVersion); - } + this.symbolFinder = new SymbolFinder(this.session.ahkVersion, readFileCacheSync); this.sendEvent(new InitializedEvent()); }) .on('warning', (warning: string) => { diff --git a/src/extension.ts b/src/extension.ts index 6d29e8ec..cc399911 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -166,6 +166,7 @@ export class AhkConfigurationProvider implements vscode.DebugConfigurationProvid useAnnounce: true, useLoadedScripts: true, useExceptionBreakpoint: false, + setHiddenBreakpoints: [], trace: false, // The following is not a configuration, but is set to pass data to the debug adapter. cancelReason: undefined, @@ -566,6 +567,52 @@ export class AhkConfigurationProvider implements vscode.DebugConfigurationProvid throw Error('`variableCategories` must be a "recommend" or array.'); })(); + // init setHiddenBreakpoints + ((): void => { + if (!Array.isArray(config.setHiddenBreakpoints)) { + throw Error('`setHiddenBreakpoints` must be a array.'); + } + + const checkHiddenBreakpoint = (value: any): boolean => { + if (!('target' in value)) { + throw Error('The `target` attribute is required.'); + } + if (!(typeof value.target === 'string' || Array.isArray(value.target))) { + throw Error('The `target` must be a string or array.'); + } + if ('condition' in value && typeof value.condition !== 'string') { + throw Error('The `condition` must be a string.'); + } + if ('hitCondition' in value && typeof value.hitCondition !== 'string') { + throw Error('The `hitCondition` must be a string.'); + } + + if (!('line' in value) || typeof value.line === 'undefined') { + value.line = 1; + } + return true; + }; + const checkHiddenBreakpointWithUI = (value: any): boolean => { + if (!('label' in value) || typeof value.label !== 'string') { + throw Error('The `label` must be a string.'); + } + if (!('breakpoints' in value) || !Array.isArray(value.breakpoints)) { + throw Error('The `breakpoints` must be a array.'); + } + + for (const breakpoint of value.breakpoints) { + checkHiddenBreakpoint(breakpoint); + } + return true; + }; + + for (const setHiddenBreakpoint of config.setHiddenBreakpoints) { + if (checkHiddenBreakpointWithUI(setHiddenBreakpoint) || checkHiddenBreakpoint(setHiddenBreakpoint)) { + continue; + } + } + })(); + // init trace ((): void => { if (typeof config.trace !== 'boolean') { diff --git a/src/util/BreakpointManager.ts b/src/util/BreakpointManager.ts index 195834e0..f3ab46f1 100644 --- a/src/util/BreakpointManager.ts +++ b/src/util/BreakpointManager.ts @@ -5,6 +5,7 @@ import { equalsIgnoreCase } from './stringUtils'; import { toFileUri } from './util'; export type BreakpointLogGroup = 'start' | 'startCollapsed' | 'end' | undefined; +export type BreakpointAction = () => Promise; export interface BreakpointAdvancedData { group?: string; condition?: string; @@ -14,9 +15,10 @@ export interface BreakpointAdvancedData { shouldBreak?: boolean; hidden?: boolean; hitCount?: number; + additionalMetaVariables?: CaseInsensitiveMap; unverifiedLine?: number; unverifiedColumn?: number; - action?: () => Promise; + action?: BreakpointAction; } export type BreakpointKind = 'breakpoint' | 'logpoint' | 'conditional breakpoint' | 'conditional logpoint'; export class Breakpoint implements BreakpointAdvancedData { @@ -33,7 +35,8 @@ export class Breakpoint implements BreakpointAdvancedData { public shouldBreak: boolean; public unverifiedLine?: number; public unverifiedColumn?: number; - public action?: () => Promise; + public additionalMetaVariables?: CaseInsensitiveMap; + public action?: BreakpointAction; constructor(dbgpBreakpoint: dbgp.Breakpoint, advancedData?: BreakpointAdvancedData) { this.id = dbgpBreakpoint.id; this.fileUri = dbgpBreakpoint.fileUri; @@ -47,6 +50,7 @@ export class Breakpoint implements BreakpointAdvancedData { this.hidden = advancedData?.hidden ?? false; this.unverifiedLine = advancedData?.unverifiedLine ?? dbgpBreakpoint.line; this.unverifiedColumn = advancedData?.unverifiedColumn; + this.additionalMetaVariables = advancedData?.additionalMetaVariables; this.action = advancedData?.action; this.shouldBreak = !(this.logMessage || this.action); @@ -131,8 +135,8 @@ export class BreakpointManager { _advancedData = advancedData; } - const response = await this.session.sendBreakpointSetCommand(fileUri, unverifiedLine); - const settedBreakpoint = new Breakpoint((await this.session.sendBreakpointGetCommand(response.id)).breakpoint, _advancedData); + const { id: breakpointId } = await this.session.sendBreakpointSetCommand(fileUri, unverifiedLine); + const settedBreakpoint = new Breakpoint((await this.session.sendBreakpointGetCommand(breakpointId)).breakpoint, _advancedData); const verifiedLine = settedBreakpoint.line; let registeredLineBreakpoints: LineBreakpoints; @@ -164,13 +168,16 @@ export class BreakpointManager { return; } - const newLineBreakpoints = new LineBreakpoints(...lineBreakpoints.filter((lineBreakpoint) => breakpoint.id !== lineBreakpoint.id)); const key = this.createKey(breakpoint.fileUri, breakpoint.line); - try { await this.session.sendBreakpointRemoveCommand(breakpoint.id); this.breakpointsMap.delete(key); - this.breakpointsMap.set(key, newLineBreakpoints); + + const breakpointWithoutTarget = lineBreakpoints.filter((lineBreakpoint) => breakpoint.id !== lineBreakpoint.id); + if (0 < breakpointWithoutTarget.length) { + const newLineBreakpoints = new LineBreakpoints(...breakpointWithoutTarget); + this.breakpointsMap.set(key, newLineBreakpoints); + } } catch { } @@ -208,6 +215,20 @@ export class BreakpointManager { } return removedBreakpoints; } + public async unregisterBreakpointGroup(groupName: string): Promise { + const targets: Breakpoint[] = []; + for (const [ , lineBreakpoint ] of this.breakpointsMap) { + for (const breakpoint of lineBreakpoint) { + if (breakpoint.group === groupName) { + targets.push(breakpoint); + } + } + } + + for await (const breakpoint of targets) { + await this.unregisterBreakpoint(breakpoint); + } + } private createKey(file: string, line: number): string { // The following encoding differences have been converted to path // file:///W%3A/project/vscode-autohotkey-debug/demo/demo.ahk" diff --git a/src/util/SymbolFinder.ts b/src/util/SymbolFinder.ts index 2cb9d1bc..de48eaf7 100644 --- a/src/util/SymbolFinder.ts +++ b/src/util/SymbolFinder.ts @@ -1,6 +1,7 @@ -import { readFileSync } from 'fs'; import { AhkVersion, ImplicitLibraryPathExtractor, IncludePathResolver } from '@zero-plusplus/autohotkey-utilities'; +import { readFileSync } from 'fs'; import { TextEditorLineNumbersStyle } from 'vscode'; +import { equalsIgnoreCase } from './stringUtils'; export interface Position { line: number; @@ -16,6 +17,8 @@ export interface Location { export type Node = | SkipNode | IncludeNode + | NamedNode; +export type NamedNode = | ClassNode | FunctionNode | DynamicPropertyNode @@ -40,6 +43,7 @@ export interface IncludeNode extends NodeBase { } export interface ClassNode extends NamedNodeBase { type: 'class'; + superClass?: string; block: Location; } export interface FunctionNode extends NamedNodeBase { @@ -73,29 +77,45 @@ export type ParserContext = { parsedNodes: Node[]; }; -export const getFullName = (node: NamedNodeBase): string => { - if (0 < node.scope.length) { - return `${node.scope.join('.')}.${node.name}`; - } - return node.name; +export const getText = (node: Node): string => { + return node.context.source.slice(node.location.startIndex, node.location.endIndex); }; -export const getLine = (node: NamedNodeBase, index: number): number => { +export const getLine = (node: Node, index: number): number => { const startLine_base1 = Array.from(node.context.source.slice(0, index).matchAll(/\r\n|\n/gu)).length + 1; return startLine_base1; }; -export const getColumn = (node: NamedNodeBase, index: number): number => { +export const getColumn = (node: Node, index: number): number => { const startColumn_base1 = (node.context.source.slice(0, index).match(/(^|(\r)?\n)(?.+)$/u)?.groups?.lastLine.length ?? 0) + 1; return startColumn_base1; }; +export const getAncestorsMap = (classNodes: ClassNode[]): Map => { + // const rootClassNode = classNodes.filter((node) => node.superClass); + const ancestorsMap = new Map(); + for (const classNode of classNodes) { + const ancestors: NamedNode[] = []; + + let currentSuperClass = classNodes.find((node) => equalsIgnoreCase(node.fullname, classNode.superClass ?? '')); + while (currentSuperClass) { + ancestors.push(currentSuperClass); + // eslint-disable-next-line @typescript-eslint/no-loop-func + currentSuperClass = classNodes.find((node) => equalsIgnoreCase(node.fullname, currentSuperClass?.superClass ?? '')); + } + ancestorsMap.set(classNode.fullname, ancestors); + } + return ancestorsMap; +}; +type FileReader = (...params: any[]) => string; export class SymbolFinder { private readonly version: AhkVersion; private readonly resolver: IncludePathResolver; private readonly implicitExtractor: ImplicitLibraryPathExtractor; - constructor(version: string | AhkVersion) { + private readonly fileReader: FileReader; + constructor(version: string | AhkVersion, fileReader?: FileReader) { this.version = typeof version === 'string' ? new AhkVersion(version) : version; this.resolver = new IncludePathResolver(this.version); this.implicitExtractor = new ImplicitLibraryPathExtractor(this.version); + this.fileReader = fileReader ?? ((filePath: string): string => readFileSync(filePath, 'utf-8')); } public find(sourceFile: string): Node[] { const context = this.createContext(sourceFile); @@ -180,7 +200,7 @@ export class SymbolFinder { this.parseIncludeNode(context, match.groups.include, match.groups.includePath); } else if (match.groups.class) { - this.parseClassNode(context, match.groups.className, match.groups.classIndent); + this.parseClassNode(context, match.groups.className, match.groups.superClassName, match.groups.classIndent); } else if (match.groups.func) { this.parseFunctionNode(context, match.groups.funcName, match.groups.funcIndent); @@ -209,7 +229,7 @@ export class SymbolFinder { } context.parsedNodes.push(this.createSkipNode(context, _contents)); } - public parseClassNode(context: ParserContext, name: string, indent: string): void { + public parseClassNode(context: ParserContext, name: string, superClassName: string, indent: string): void { const startIndex = context.index; const stack: string[] = []; context.scope.push(name); @@ -242,7 +262,7 @@ export class SymbolFinder { } if (match?.groups?.class) { - this.parseClassNode(context, match.groups.className, match.groups.classIndent); + this.parseClassNode(context, match.groups.className, match.groups.superClassName, match.groups.classIndent); } else if (match?.groups?.func) { this.parseFunctionNode(context, match.groups.funcName, match.groups.funcIndent); @@ -264,7 +284,7 @@ export class SymbolFinder { const location = this.createLocation(context, startIndex, context.index); const blockLocation = this.createLocation(context, startBlockIndent, context.index); - const classNode = this.createClassNode(context, name, location, blockLocation); + const classNode = this.createClassNode(context, name, superClassName, location, blockLocation); context.parsedNodes.push(classNode); } public parseIncludeNode(context: ParserContext, includeLine: string, includePath: string): void { @@ -463,7 +483,7 @@ export class SymbolFinder { }); } public createContext(sourceFile: string, prevContext?: ParserContext): ParserContext { - const rootSource = readFileSync(sourceFile, 'utf-8'); + const rootSource = this.fileReader(sourceFile); const context: ParserContext = { ...prevContext, sourceFile, @@ -498,12 +518,13 @@ export class SymbolFinder { location: this.createLocation(context, context.index, context.index + contents.length), }; } - public createClassNode(context: ParserContext, name: string, location: Location, blockLocation: Location): ClassNode { + public createClassNode(context: ParserContext, name: string, superClassName: string, location: Location, blockLocation: Location): ClassNode { return { context, type: 'class', name, fullname: this.createFullName(context, name), + superClass: superClassName, scope: context.scope.slice(), location, block: blockLocation, diff --git a/src/util/evaluator/ExpressionEvaluator.ts b/src/util/evaluator/ExpressionEvaluator.ts index 4ccfa066..acaaba10 100644 --- a/src/util/evaluator/ExpressionEvaluator.ts +++ b/src/util/evaluator/ExpressionEvaluator.ts @@ -1033,7 +1033,7 @@ export class ExpressionEvaluator { return ''; } - const name = await this.nodeToString(node.arguments[0]); + const name = await this.evalNode(node.arguments[0]); const maxDepth = 2 <= node.arguments.length ? Number(node.arguments[1]) : 1; diff --git a/src/util/util.ts b/src/util/util.ts index 431f50c3..7e3cb0b8 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { statSync } from 'fs'; +import * as path from 'path'; +import { promises as fs, readFileSync, statSync } from 'fs'; import { AhkVersion } from '@zero-plusplus/autohotkey-utilities'; import { range } from 'lodash'; import tcpPortUsed from 'tcp-port-used'; import { URI } from 'vscode-uri'; +import { CaseInsensitiveMap } from './CaseInsensitiveMap'; export const isDirectory = (dirPath: string): boolean => { try { @@ -192,3 +194,25 @@ export const getUnusedPort = async(hostname: string, start: number, end: number) } throw Error(`All ports in the specified range (${start}-${end}) are in use.`); }; + +const fileCache = new CaseInsensitiveMap(); +export const readFileCache = async(filePath: string): Promise => { + const _filePath = path.resolve(filePath); + if (fileCache.has(_filePath)) { + return fileCache.get(_filePath)!; + } + + const source = await fs.readFile(_filePath, 'utf-8'); + fileCache.set(_filePath, source); + return source; +}; +export const readFileCacheSync = (filePath: string): string => { + const _filePath = path.resolve(filePath); + if (fileCache.has(_filePath)) { + return fileCache.get(_filePath)!; + } + + const source = readFileSync(_filePath, 'utf-8'); + fileCache.set(_filePath, source); + return source; +};