diff --git a/.vscode/settings.json b/.vscode/settings.json index 713e687..560c672 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "typescript.updateImportsOnFileMove.enabled": "prompt", "mochaExplorer.files": "./data-extraction/test/**/*.test.ts", "mochaExplorer.require": ["ts-node/register", "source-map-support/register"], - "tasksStatusbar.taskLabelFilter": "dev" + "tasksStatusbar.taskLabelFilter": "dev", + "editor.formatOnSave": true } diff --git a/data-extraction/src/CommonDataTypes.ts b/data-extraction/src/CommonDataTypes.ts index e1d2015..cd57eec 100644 --- a/data-extraction/src/CommonDataTypes.ts +++ b/data-extraction/src/CommonDataTypes.ts @@ -107,7 +107,7 @@ export type GraphEdge = { label?: string; id?: string; color?: string; - dashes?: boolean; + style?: "solid" | "dashed" | "dotted"; }; export type GridVisualizationData = { @@ -157,12 +157,38 @@ export type MonacoTextVisualizationData = { * The text to show */ text: string; + decorations?: { + range: LineColumnRange; + label?: string; + }[]; /** * An optional filename that might be used for chosing a syntax highlighter */ fileName?: string; }; +export type LineColumnRange = { + /** + * The start position + */ + start: LineColumnPosition; + /** + * The end position + */ + end: LineColumnPosition; +}; + +export type LineColumnPosition = { + /** + * The 0-based line number + */ + line: number; + /** + * The 0-based column number + */ + column: number; +}; + export type MonacoTextDiffVisualizationData = { kind: { text: true; diff --git a/data-extraction/src/js/api/DataExtractorApi.ts b/data-extraction/src/js/api/DataExtractorApi.ts index 0fd8958..8f4cfc1 100644 --- a/data-extraction/src/js/api/DataExtractorApi.ts +++ b/data-extraction/src/js/api/DataExtractorApi.ts @@ -22,7 +22,8 @@ export interface DataExtractorApi { getData( valueFn: () => unknown, evalFn: (expression: string) => T, - preferredDataExtractorId: string | undefined + preferredDataExtractorId: string | undefined, + variablesInScope: Record ): JSONString; /** @@ -68,6 +69,8 @@ export interface DataExtractorContext { * Evaluates an expression in the context of the active stack frame. */ evalFn: (expression: string) => TEval; + + variablesInScope: Record; } export interface DataExtraction { diff --git a/data-extraction/src/js/api/DataExtractorApiImpl.ts b/data-extraction/src/js/api/DataExtractorApiImpl.ts index 3f5d644..ef4c864 100644 --- a/data-extraction/src/js/api/DataExtractorApiImpl.ts +++ b/data-extraction/src/js/api/DataExtractorApiImpl.ts @@ -14,6 +14,9 @@ import { registerDefaultExtractors } from "./default-extractors"; */ export class DataExtractorApiImpl implements DataExtractorApi { public static lastEvalFn: ((expression: string) => T) | undefined; + public static lastVariablesInScope: + | Record + | undefined = undefined; private readonly extractors = new Map(); @@ -34,7 +37,8 @@ export class DataExtractorApiImpl implements DataExtractorApi { public getData( valueFn: () => unknown, evalFn: (expression: string) => T, - preferredDataExtractorId: string | undefined + preferredDataExtractorId: string | undefined, + variablesInScope: Record ): JSONString { const extractions = new Array(); const extractionCollector: ExtractionCollector = { @@ -43,13 +47,20 @@ export class DataExtractorApiImpl implements DataExtractorApi { }, }; + DataExtractorApiImpl.lastVariablesInScope = variablesInScope; DataExtractorApiImpl.lastEvalFn = evalFn; const value = valueFn(); - DataExtractorApiImpl.lastEvalFn = undefined; for (const e of this.extractors.values()) { - e.getExtractions(value, extractionCollector, { evalFn }); + e.getExtractions(value, extractionCollector, { + evalFn, + variablesInScope, + }); } + + DataExtractorApiImpl.lastVariablesInScope = undefined; + DataExtractorApiImpl.lastEvalFn = undefined; + extractions.sort((a, b) => b.priority - a.priority); let usedExtraction = extractions[0]; if (!usedExtraction) { diff --git a/data-extraction/src/js/api/default-extractors/StringDiffExtractor.ts b/data-extraction/src/js/api/default-extractors/StringDiffExtractor.ts new file mode 100644 index 0000000..fac8205 --- /dev/null +++ b/data-extraction/src/js/api/default-extractors/StringDiffExtractor.ts @@ -0,0 +1,22 @@ +import { DataExtractor, DataExtractorContext, ExtractionCollector } from "../.."; + +export class StringDiffExtractor implements DataExtractor { + public readonly id = "string-diff"; + + public getExtractions(data: unknown, + collector: ExtractionCollector, + context: DataExtractorContext) { + if (Array.isArray(data) && data.length === 2 && typeof data[0] === "string" && typeof data[1] === "string") { + collector.addExtraction({ + id: "string-diff", + name: "String Diff", + priority: 1000, + extractData: () => ({ + kind: { text: true }, + text: data[0], + otherText: data[1] + }) + }) + } + } +} \ No newline at end of file diff --git a/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts b/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts index f3711b8..9ba7e82 100644 --- a/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts +++ b/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts @@ -8,6 +8,8 @@ import { ObjectGraphExtractor } from "./ObjectGraphExtractor"; import { getDataExtractorApi } from "../injection"; import { GridExtractor } from "./GridExtractor"; import { TableDataExtractor } from "./TableExtractor"; +import { StringDiffExtractor } from "./StringDiffExtractor"; +import { find } from "../../helpers"; /** * The default data extractors should be registered by VS Code automatically. @@ -25,7 +27,102 @@ export function registerDefaultExtractors( new ObjectGraphExtractor(), new GridExtractor(), new TableDataExtractor(), + new StringDiffExtractor(), ]) { api.registerExtractor(item); } + + function isRange( + value: unknown + ): value is { + startColumn: number; + startLineNumber: number; + endColumn: number; + endLineNumber: number; + } { + return ( + typeof value === "object" && + !!value && + "startColumn" in value && + "startLineNumber" in value + ); + } + + function isPosition( + value: unknown + ): value is { column: number; lineNumber: number } { + return ( + typeof value === "object" && + !!value && + "column" in value && + "lineNumber" in value + ); + } + + // hedietDbgVis.find(x => x.constructor.name === 'TextModel').getValue() + api.registerExtractor({ + id: "positionInTextModel", + getExtractions(data, collector, context) { + let range: { + start: { + line: number; + column: number; + }; + end: { + line: number; + column: number; + }; + }; + if (isRange(data)) { + range = { + start: { + line: data.startLineNumber - 1, + column: data.startColumn - 1, + }, + end: { + line: data.endLineNumber - 1, + column: data.endColumn - 1, + }, + }; + } else if (isPosition(data)) { + range = { + start: { + line: data.lineNumber - 1, + column: data.column - 1, + }, + end: { + line: data.lineNumber - 1, + column: data.column - 1, + }, + }; + } else { + return; + } + + if (!isRange(data) && !isPosition(data)) { + return; + } + + const textModel: any = find( + x => + typeof x === "object" && + !!x && + "constructor" in x && + (x as any).constructor.name === "TextModel" + ); + + collector.addExtraction({ + id: "positionInTextModel", + name: "Position In TextModel", + extractData() { + return { + kind: { text: true }, + text: textModel.getValue(), + decorations: [{ range }], + }; + }, + priority: 1000, + }); + }, + }); } diff --git a/data-extraction/src/js/helpers/createGraph.ts b/data-extraction/src/js/helpers/createGraph.ts index 77aa6d4..3160075 100644 --- a/data-extraction/src/js/helpers/createGraph.ts +++ b/data-extraction/src/js/helpers/createGraph.ts @@ -4,7 +4,8 @@ import { GraphVisualizationData, } from "../../CommonDataTypes"; -export type CreateGraphEdge = { to: T } & Omit; +export type CreateGraphEdge = ({ to: T } | { from: T } | { include: T }) & + Omit; /** * Given a list of roots, it creates a graph by following their edges recursively. @@ -58,13 +59,31 @@ export function createGraph( const fromId = getId(item); r.nodes.push({ ...nodeInfo, id: fromId, ["edges" as any]: undefined }); for (const e of nodeInfo.edges) { - const toId = getId(e.to); - r.edges.push({ - ...e, - from: fromId, - to: toId, - }); - let shouldPush = !processed.has(e.to); + let other: T; + let toId: string | undefined; + let fromId_: string | undefined; + if ("to" in e) { + other = e.to; + toId = getId(e.to); + fromId_ = fromId; + } else if ("from" in e) { + other = e.from; + toId = getId(e.from); + fromId_ = fromId; + } else if ("include" in e) { + other = e.include; + } else { + throw new Error("Every edge must either have 'to' or 'from'"); + } + + if (fromId_ !== undefined && toId !== undefined) { + r.edges.push({ + ...e, + from: fromId_, + to: toId, + }); + } + let shouldPush = !processed.has(other); if ( options.maxSize && processed.size + queue.length > options.maxSize @@ -72,7 +91,7 @@ export function createGraph( shouldPush = false; } if (shouldPush) { - queue.push({ item: e.to, dist: dist + 1 }); + queue.push({ item: other, dist: dist + 1 }); } } } diff --git a/data-extraction/src/js/helpers/find.ts b/data-extraction/src/js/helpers/find.ts new file mode 100644 index 0000000..bbb1035 --- /dev/null +++ b/data-extraction/src/js/helpers/find.ts @@ -0,0 +1,34 @@ +import { DataExtractorApiImpl } from "../api"; + +export function find(predicate: (obj: unknown) => boolean): unknown { + const processed = new Set(); + if (!DataExtractorApiImpl.lastVariablesInScope) { + throw new Error("No variables in scope!"); + } + + const values = Object.values(DataExtractorApiImpl.lastVariablesInScope); + const queue = [...values]; + + let i = 10000; + while (i > 0) { + i--; + + const top = queue.shift(); + processed.add(top); + + if (predicate(top)) { + return top; + } + + if (typeof top === "object" && top) { + for (const val of Object.values(top)) { + if (!processed.has(val)) { + processed.add(val); + queue.push(val); + } + } + } + } + + return undefined; +} diff --git a/data-extraction/src/js/helpers/index.ts b/data-extraction/src/js/helpers/index.ts index 4f528cd..a9bd90a 100644 --- a/data-extraction/src/js/helpers/index.ts +++ b/data-extraction/src/js/helpers/index.ts @@ -4,3 +4,4 @@ export * from "./tryEval"; export * from "./markedGrid"; export * from "./cache"; export * from "./asData"; +export * from "./find"; diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index ceccb98..869733f 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 2.1.0 + +- Adds text diff and text decoration viewer. +- Supports more js debug adapters + ## 2.0.6 - Fixes grid extractor bug diff --git a/extension/package.json b/extension/package.json index d39de5b..fa333b0 100644 --- a/extension/package.json +++ b/extension/package.json @@ -4,7 +4,7 @@ "displayName": "Debug Visualizer", "description": "A visual watch window that lets you visualize your data structures while debugging.", "icon": "docs/logo.drawio.png", - "version": "2.0.6", + "version": "2.1.0", "license": "GPL-3.0", "engines": { "vscode": "^1.46.0" diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts index 024cca6..5ccd924 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts @@ -26,7 +26,11 @@ export class JsEvaluationEngine implements EvaluationEngine { "chrome", "pwa-chrome", "pwa-node", + "pwa-extensionHost", + "node-terminal", + "pwa-msedge", ]; + if (supportedDebugAdapters.indexOf(session.session.type) !== -1) { return new JsEvaluator(session); } @@ -54,6 +58,31 @@ class JsEvaluator implements Evaluator { | { kind: "data"; result: DataExtractionResult } | { kind: "error"; message: FormattedMessage } > { + const variableNames = new Array(); + if (frameId) { + const scopes = await this.session.getScopes({ frameId }); + const scopeVariables = await Promise.all( + scopes + .filter(s => !s.expensive) + .map(s => + this.session.getVariables({ + variablesReference: s.variablesReference, + }) + ) + ); + for (const variables of scopeVariables) { + variableNames.push( + ...variables + .filter(v => v.value !== "undefined") + .map(v => v.name) + ); + } + + if (variableNames.length > 100) { + variableNames.length = 100; + } + } + while (true) { try { const preferredExtractorExpr = preferredExtractorId @@ -63,7 +92,8 @@ class JsEvaluator implements Evaluator { const body = `${getExpressionForDataExtractorApi()}.getData( e => (${expression}), expr => eval(expr), - ${preferredExtractorExpr} + ${preferredExtractorExpr}, + {${variableNames.map(n => `${n}: ${n}`).join(",")}}, )`; const wrappedExpr = ` diff --git a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts index 537cd60..47b6041 100644 --- a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts +++ b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts @@ -5,11 +5,13 @@ import { } from "./EvaluationWatchService"; import { observable, autorun, action } from "mobx"; import { Disposable } from "@hediet/std/disposable"; +import { CancellationTokenSource, CancellationToken } from "vscode"; import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction"; import { DataExtractionState, CompletionItem } from "../webviewContract"; import { hotClass } from "@hediet/node-reload"; import { VsCodeDebuggerView } from "../debugger/VsCodeDebuggerView"; import { EvaluationEngine } from "./EvaluationEngine/EvaluationEngine"; +import { wait } from "@hediet/std/timer"; @hotClass(module) export class EvaluationWatchServiceImpl implements EvaluationWatchService { @@ -63,7 +65,25 @@ export class EvaluationWatchServiceImpl implements EvaluationWatchService { return evaluator.languageId; } - public async refresh(w: ObservableEvaluationWatcher): Promise { + private runningRefreshOperation: Disposable | undefined; + + public refresh(w: ObservableEvaluationWatcher): void { + if (this.runningRefreshOperation) { + this.runningRefreshOperation.dispose(); + } + const tokenSource = new CancellationTokenSource(); + this.runningRefreshOperation = { + dispose: () => { + tokenSource.cancel(); + }, + }; + this._refresh(w, tokenSource.token); + } + + private async _refresh( + w: ObservableEvaluationWatcher, + token: CancellationToken + ): Promise { const session = this.vsCodeDebuggerView.activeDebugSession; if (!session) { w._state = { kind: "noDebugSession" }; @@ -89,6 +109,15 @@ export class EvaluationWatchServiceImpl implements EvaluationWatchService { preferredExtractorId: w.preferredDataExtractor, }); + if (result.kind === "error") { + // give cancellation requested more time in case of errors + await wait(330); + } + + if (token.isCancellationRequested) { + return; + } + w._state = result; } diff --git a/extension/src/debugger/EnhancedDebugSession.ts b/extension/src/debugger/EnhancedDebugSession.ts index 5e8a00e..21acb9c 100644 --- a/extension/src/debugger/EnhancedDebugSession.ts +++ b/extension/src/debugger/EnhancedDebugSession.ts @@ -46,12 +46,27 @@ export class EnhancedDebugSession { } } + public async getScopes(args: { frameId: number }): Promise { + try { + const reply = await this.session.customRequest("scopes", { + frameId: args.frameId, + }); + if (!reply) { + return []; + } + return reply.scopes; + } catch (error) { + console.error(error); + return []; + } + } + public async getVariables(args: { - variablesReference: number + variablesReference: number; }): Promise { try { const reply = await this.session.customRequest("variables", { - variablesReference: args.variablesReference + variablesReference: args.variablesReference, }); if (!reply) { return []; @@ -72,16 +87,25 @@ export class EnhancedDebugSession { expression: string; frameId: number | undefined; context: "watch" | "repl" | "copy"; - }): Promise<{ result: string, variablesReference: number }> { + }): Promise<{ result: string; variablesReference: number }> { const reply = await this.session.customRequest("evaluate", { expression: args.expression, frameId: args.frameId, context: args.context, }); - return { result: reply.result, variablesReference: reply.variablesReference }; + return { + result: reply.result, + variablesReference: reply.variablesReference, + }; } } +interface Scope { + name: string; + expensive: boolean; + variablesReference: number; +} + interface Variable { name: string; value: string; diff --git a/webview/package.json b/webview/package.json index e436518..b4b3903 100644 --- a/webview/package.json +++ b/webview/package.json @@ -20,8 +20,8 @@ "react-dom": "^16.12.0", "react-measure": "^2.3.0", "@hediet/debug-visualizer-data-extraction": "*", - "@hediet/visualization-bundle": "^0.1.6", - "@hediet/visualization-core": "^0.1.1", + "@hediet/visualization-bundle": "^0.2.1", + "@hediet/visualization-core": "^0.2.0", "@knuddels/mobx-logger": "^1.1.1", "less-loader": "^6.2.0" }, diff --git a/yarn.lock b/yarn.lock index ce6d595..03648c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -507,10 +507,10 @@ stack-trace "^0.0.10" ws "^7.0.0" -"@hediet/semantic-json@^0.3.14": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@hediet/semantic-json/-/semantic-json-0.3.14.tgz#876bd5c4b646c134641ad17e9ef352595fb163e7" - integrity sha512-u2QGfT98RNrcQjqRHwFUy+TU8+sLneBOYvHHNbkHyYNokqi8kCpE9LReTygGT+6SfMtydTWN6JT9L2ffxrvV6w== +"@hediet/semantic-json@^0.3.15": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@hediet/semantic-json/-/semantic-json-0.3.15.tgz#e38f799b1a7dd2624a3ee2e05bca58cfe76cb58d" + integrity sha512-8t1NXiYDF2wqIt1jEknxNnpucdtgFAI2MGSWnnewDhaYqWOJLMY/MxH60gDX03vycf2ySb6vEYpEwsiB6r0O3Q== "@hediet/std@^0.6.0": version "0.6.0" @@ -550,16 +550,16 @@ "@hediet/std" "^0.6.0" io-ts "^1.8.6" -"@hediet/visualization-bundle@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@hediet/visualization-bundle/-/visualization-bundle-0.1.6.tgz#a8ab11be910a30d0d26a88044caf11cab4a60f98" - integrity sha512-IQZDORYlRTojBwJUIADpg4NRxCqK6Q+bl1H2IP7Bq28wdwsRtuERB2UcGPufn00NGhJbOw6FAH2e1zwsQAkboQ== +"@hediet/visualization-bundle@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@hediet/visualization-bundle/-/visualization-bundle-0.2.1.tgz#397a6bff0ea12018a7a1cb1c182ef8c5de87328f" + integrity sha512-Qfyn8WbWGwuE5J1z8U7l8E5VKuG3DpS2JJOtKhlW1QW/rhqBWOLd5avifxqjWxiMIfqh3NTShp9gLHpmI2Bysw== dependencies: "@finos/perspective-viewer" "^0.5.2" "@finos/perspective-viewer-d3fc" "^0.5.2" "@finos/perspective-viewer-datagrid" "^0.5.2" "@hediet/monaco-editor-react" "^0.1.1" - "@hediet/semantic-json" "^0.3.14" + "@hediet/semantic-json" "^0.3.15" "@hediet/std" "^0.6.0" classnames "^2.2.6" line-column "^1.0.2" @@ -576,12 +576,12 @@ vis-network "^8.0.0" viz.js "^2.1.2" -"@hediet/visualization-core@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@hediet/visualization-core/-/visualization-core-0.1.1.tgz#240b96ac48e24b7ae81f2b0360cebd57091eb600" - integrity sha512-2WNP75TZg87bejt5XB+11jzbAoJ5EGbVrW+srSHRjUSdezZQ53x16E4XVV/q9W6HD6okTuz7wGjDrapNzK1EMw== +"@hediet/visualization-core@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@hediet/visualization-core/-/visualization-core-0.2.0.tgz#8fca0044d5d91e8c45354984ae9c131ef7850a76" + integrity sha512-r/SYDUipaUSdu1JXL1X0Le5ZkBXVGbKXo6jkX0jFq3NPz/60+B4MnIQnk8D7TCWLXftGFtE6MSFejrE+miup4w== dependencies: - "@hediet/semantic-json" "^0.3.14" + "@hediet/semantic-json" "^0.3.15" "@hediet/std" "^0.6.0" react "^16.12.0" react-dom "^16.12.0"