Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da0e350b8 | ||
|
|
f220fa8df7 | ||
|
|
13a28ae1bb | ||
|
|
f254c3a31c | ||
|
|
18acf1d7f4 | ||
|
|
96c26e5388 | ||
|
|
e0fbe007fa | ||
|
|
f4f227428d | ||
|
|
3d49570341 | ||
|
|
f8c740a317 | ||
|
|
8b4fc34802 | ||
|
|
b66c30b610 | ||
|
|
1d70b99f19 | ||
|
|
84762b2413 | ||
|
|
a9656e1e12 | ||
|
|
d45dfdaa71 | ||
|
|
904d0060ea | ||
|
|
1b6fabfe29 | ||
|
|
a368584adb | ||
|
|
b6a7d5e869 | ||
|
|
9684af8f94 | ||
|
|
d20d421c37 | ||
|
|
017cc54e17 | ||
|
|
5668efba0a | ||
|
|
307adad2b0 | ||
|
|
51370a519d | ||
|
|
7551b764f1 | ||
|
|
2453c8bf9c | ||
|
|
9fb1e8127a | ||
|
|
a2a5d1b873 | ||
|
|
e7e76ccfb9 | ||
|
|
e01c09eac5 | ||
|
|
86e71ae534 | ||
|
|
e46cdef402 | ||
|
|
2af8ef9a53 | ||
|
|
aa3412e6ac | ||
|
|
6543180591 | ||
|
|
cd88168211 | ||
|
|
683f1e1f76 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -18,6 +18,6 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 20.x
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
target
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
@@ -81,3 +82,10 @@ typings/
|
|||||||
|
|
||||||
dist/
|
dist/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
# Python byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
*.vsix
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"useTabs": true
|
"useTabs": true,
|
||||||
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
|
|||||||
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@@ -53,7 +53,8 @@
|
|||||||
"HOT_RELOAD": "",
|
"HOT_RELOAD": "",
|
||||||
"USE_DEV_UI": ""
|
"USE_DEV_UI": ""
|
||||||
},
|
},
|
||||||
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"]
|
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"],
|
||||||
|
"preLaunchTask": "npm: build - extension"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Extension (Dev UI)",
|
"name": "Run Extension (Dev UI)",
|
||||||
@@ -76,6 +77,6 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Run Dart samples",
|
"name": "Run Dart samples",
|
||||||
"program": "demos/dart/demo.dart"
|
"program": "demos/dart/demo.dart"
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -7,11 +7,15 @@
|
|||||||
"plantuml.exportOutDir": "docs/exported",
|
"plantuml.exportOutDir": "docs/exported",
|
||||||
"typescript.updateImportsOnFileMove.enabled": "prompt",
|
"typescript.updateImportsOnFileMove.enabled": "prompt",
|
||||||
"mochaExplorer.files": "./data-extraction/test/**/*.test.ts",
|
"mochaExplorer.files": "./data-extraction/test/**/*.test.ts",
|
||||||
"mochaExplorer.require": [
|
"mochaExplorer.require": ["ts-node/register", "source-map-support/register"],
|
||||||
"ts-node/register",
|
|
||||||
"source-map-support/register"
|
|
||||||
],
|
|
||||||
"tasksStatusbar.taskLabelFilter": "dev",
|
"tasksStatusbar.taskLabelFilter": "dev",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
|
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"prettier.prettierPath": "node_modules/prettier",
|
||||||
|
"prettier.printWidth": 120,
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
.vscode/tasks.json
vendored
13
.vscode/tasks.json
vendored
@@ -15,6 +15,19 @@
|
|||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"label": "npm: build - extension",
|
||||||
|
// Deliberately the ROOT package.json's "build" script (data-extraction
|
||||||
|
// -> webview -> extension, in that order), not extension/'s own build
|
||||||
|
// script -- the extension's webpack just *copies* whatever is already
|
||||||
|
// sitting in webview/dist (via the node_modules/debug-visualizer-webview
|
||||||
|
// symlink) into its own bundle rather than rebuilding it. Scoping this
|
||||||
|
// task to extension/ alone silently ships a stale webview bundle.
|
||||||
|
"script": "build",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"@types/copy-webpack-plugin": "^10.1.0",
|
||||||
"@types/mocha": "^7.0.2",
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/node": "^13.7.4",
|
"@types/node": "^13.7.4",
|
||||||
"@types/plotly.js": "1.44.28",
|
"@types/plotly.js": "1.44.28",
|
||||||
@@ -36,10 +38,11 @@
|
|||||||
"mocha-lcov-reporter": "^1.3.0",
|
"mocha-lcov-reporter": "^1.3.0",
|
||||||
"nyc": "^15.0.0",
|
"nyc": "^15.0.0",
|
||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16",
|
||||||
"ts-loader": "^6.2.1",
|
"tslint": "^6.1.3",
|
||||||
"tslint": "^6.0.0",
|
"typescript": "^5.1.6",
|
||||||
"typescript": "^3.8.2",
|
"webpack": "^5.88.1",
|
||||||
"webpack": "^4.41.6",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-cli": "^3.3.11"
|
"ts-loader": "^9.4.4",
|
||||||
|
"ts-node": "^10.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,16 @@ export type PlotlyVisualizationData = {
|
|||||||
text?: string | string[];
|
text?: string | string[];
|
||||||
xaxis?: string;
|
xaxis?: string;
|
||||||
yaxis?: string;
|
yaxis?: string;
|
||||||
|
cells?: {
|
||||||
|
values?: string[][];
|
||||||
|
};
|
||||||
|
header?: {
|
||||||
|
values?: string[];
|
||||||
|
};
|
||||||
|
domain?: {
|
||||||
|
x?: number[],
|
||||||
|
y?: number[],
|
||||||
|
};
|
||||||
x?: (string | number | null)[] | (string | number | null)[][];
|
x?: (string | number | null)[] | (string | number | null)[][];
|
||||||
y?: (string | number | null)[] | (string | number | null)[][];
|
y?: (string | number | null)[] | (string | number | null)[][];
|
||||||
z?: (string | number | null)[] | (string | number | null)[][];
|
z?: (string | number | null)[] | (string | number | null)[][];
|
||||||
@@ -257,7 +267,8 @@ export type PlotlyVisualizationData = {
|
|||||||
| "waterfall"
|
| "waterfall"
|
||||||
| "funnel"
|
| "funnel"
|
||||||
| "funnelarea"
|
| "funnelarea"
|
||||||
| "scattermapbox";
|
| "scattermapbox"
|
||||||
|
| "table";
|
||||||
mode?:
|
mode?:
|
||||||
| "lines"
|
| "lines"
|
||||||
| "markers"
|
| "markers"
|
||||||
|
|||||||
10
data-extraction/src/getGlobal.ts
Normal file
10
data-extraction/src/getGlobal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function getGlobal(): any {
|
||||||
|
if (typeof globalThis === "object") {
|
||||||
|
return globalThis;
|
||||||
|
} else if (typeof global === "object") {
|
||||||
|
return global;
|
||||||
|
} else if (typeof window === "object") {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
throw new Error("No global available");
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import { DataExtractionResult, VisualizationData } from "../../DataExtractionResult";
|
||||||
VisualizationData,
|
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
|
||||||
DataExtractionResult,
|
|
||||||
} from "../../DataExtractionResult";
|
|
||||||
|
|
||||||
export interface DataExtractorApi {
|
export interface DataExtractorApi {
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +21,8 @@ export interface DataExtractorApi {
|
|||||||
valueFn: () => unknown,
|
valueFn: () => unknown,
|
||||||
evalFn: <T>(expression: string) => T,
|
evalFn: <T>(expression: string) => T,
|
||||||
preferredDataExtractorId: string | undefined,
|
preferredDataExtractorId: string | undefined,
|
||||||
variablesInScope: Record<string, unknown>
|
variablesInScope: Record<string, unknown>,
|
||||||
|
callFramesSnapshot: CallFramesSnapshot | null
|
||||||
): JSONString<DataResult>;
|
): JSONString<DataResult>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +30,8 @@ export interface DataExtractorApi {
|
|||||||
* @preferExisting if `true`, existing extractors with the same id are not overwritten.
|
* @preferExisting if `true`, existing extractors with the same id are not overwritten.
|
||||||
*/
|
*/
|
||||||
registerDefaultExtractors(preferExisting?: boolean): void;
|
registerDefaultExtractors(preferExisting?: boolean): void;
|
||||||
|
|
||||||
|
setDataExtractorFn(id: string, fn: LoadDataExtractorsFn | undefined): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataResult =
|
export type DataResult =
|
||||||
@@ -39,22 +40,52 @@ export type DataResult =
|
|||||||
extractionResult: DataExtractionResult;
|
extractionResult: DataExtractionResult;
|
||||||
}
|
}
|
||||||
| { kind: "NoExtractors" }
|
| { kind: "NoExtractors" }
|
||||||
| { kind: "Error"; message: string };
|
| { kind: "Error"; message: string }
|
||||||
|
| {
|
||||||
|
kind: "OutdatedCallFrameSnapshot";
|
||||||
|
callFramesRequest: CallFramesRequest;
|
||||||
|
};
|
||||||
|
|
||||||
export interface JSONString<T> extends String {
|
export interface JSONString<T> extends String {
|
||||||
__brand: { json: T };
|
__brand: { json: T };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CallFramesRequest {
|
||||||
|
requestId: string;
|
||||||
|
requestedCallFrames: CallFrameRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallFrameRequest {
|
||||||
|
methodName: string;
|
||||||
|
pathRegExp: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallFramesSnapshot {
|
||||||
|
requestId: string;
|
||||||
|
frames: (CallFrameInfo | SkippedCallFrames)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkippedCallFrames {
|
||||||
|
skippedFrames: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallFrameInfo {
|
||||||
|
methodName: string;
|
||||||
|
source: { name: string; path: string };
|
||||||
|
vars: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataExtractor {
|
export interface DataExtractor {
|
||||||
/**
|
/**
|
||||||
* Must be unique among all data extractors.
|
* Must be unique among all data extractors.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
getExtractions(
|
|
||||||
data: unknown,
|
/**
|
||||||
extractionCollector: ExtractionCollector,
|
* Filters the data to be extracted.
|
||||||
context: DataExtractorContext
|
*/
|
||||||
): void;
|
dataCtor?: string;
|
||||||
|
getExtractions(data: unknown, extractionCollector: ExtractionCollector, context: DataExtractorContext): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtractionCollector {
|
export interface ExtractionCollector {
|
||||||
@@ -70,7 +101,15 @@ export interface DataExtractorContext {
|
|||||||
*/
|
*/
|
||||||
evalFn: <TEval>(expression: string) => TEval;
|
evalFn: <TEval>(expression: string) => TEval;
|
||||||
|
|
||||||
variablesInScope: Record<string, unknown>;
|
readonly expression: string | undefined;
|
||||||
|
|
||||||
|
readonly variablesInScope: Record<string, () => unknown>;
|
||||||
|
|
||||||
|
extract(value: unknown): VisualizationData | undefined;
|
||||||
|
|
||||||
|
readonly callFrameInfos: readonly (CallFrameInfo | SkippedCallFrames)[];
|
||||||
|
|
||||||
|
addCallFrameRequest(request: CallFrameRequest): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataExtraction {
|
export interface DataExtraction {
|
||||||
@@ -83,10 +122,12 @@ export interface DataExtraction {
|
|||||||
* A unique id identifying this extraction among all extractions.
|
* A unique id identifying this extraction among all extractions.
|
||||||
* Required to express extraction preferences.
|
* Required to express extraction preferences.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user friendly name of this extraction.
|
* A user friendly name of this extraction.
|
||||||
*/
|
*/
|
||||||
name: string;
|
name?: string;
|
||||||
|
|
||||||
extractData(): VisualizationData;
|
extractData(): VisualizationData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
import {
|
import {
|
||||||
|
DataExtractorInfo,
|
||||||
|
VisualizationData,
|
||||||
|
} from "../../DataExtractionResult";
|
||||||
|
import * as helpers from "../helpers";
|
||||||
|
import {
|
||||||
|
CallFrameInfo,
|
||||||
|
CallFrameRequest,
|
||||||
|
CallFramesSnapshot,
|
||||||
|
DataExtraction,
|
||||||
|
DataExtractor,
|
||||||
DataExtractorApi,
|
DataExtractorApi,
|
||||||
|
DataExtractorContext,
|
||||||
DataResult,
|
DataResult,
|
||||||
JSONString,
|
JSONString,
|
||||||
DataExtractor,
|
SkippedCallFrames,
|
||||||
DataExtraction,
|
|
||||||
ExtractionCollector,
|
|
||||||
} from "./DataExtractorApi";
|
} from "./DataExtractorApi";
|
||||||
import { DataExtractorInfo } from "../../DataExtractionResult";
|
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
|
||||||
import { registerDefaultExtractors } from "./default-extractors";
|
import { registerDefaultExtractors } from "./default-extractors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class DataExtractorApiImpl implements DataExtractorApi {
|
export class DataExtractorApiImpl implements DataExtractorApi {
|
||||||
public static lastEvalFn: (<T>(expression: string) => T) | undefined;
|
public static lastContext: DataExtractorContext | undefined = undefined;
|
||||||
public static lastVariablesInScope:
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined = undefined;
|
|
||||||
|
|
||||||
private readonly extractors = new Map<string, DataExtractor>();
|
private readonly extractors = new Map<string, DataExtractor>();
|
||||||
|
private readonly extractorSources = new Map<string, LoadDataExtractorsFn>();
|
||||||
|
|
||||||
private toJson<TData>(data: TData): JSONString<TData> {
|
private toJson<TData>(data: TData): JSONString<TData> {
|
||||||
return JSON.stringify(data) as any;
|
return JSON.stringify(data) as any;
|
||||||
@@ -38,30 +45,39 @@ export class DataExtractorApiImpl implements DataExtractorApi {
|
|||||||
valueFn: () => unknown,
|
valueFn: () => unknown,
|
||||||
evalFn: <T>(expression: string) => T,
|
evalFn: <T>(expression: string) => T,
|
||||||
preferredDataExtractorId: string | undefined,
|
preferredDataExtractorId: string | undefined,
|
||||||
variablesInScope: Record<string, unknown>
|
variablesInScope: Record<string, () => unknown>,
|
||||||
|
callFramesSnapshot: CallFramesSnapshot | null
|
||||||
): JSONString<DataResult> {
|
): JSONString<DataResult> {
|
||||||
const extractions = new Array<DataExtraction>();
|
const callFrameRequests: CallFrameRequest[] = [];
|
||||||
const extractionCollector: ExtractionCollector = {
|
const rootContext = new ContextImpl(
|
||||||
addExtraction(extraction) {
|
variablesInScope,
|
||||||
extractions.push(extraction);
|
removeEnd(removeStart(valueFn.toString(), "() => ("), ")").trim(),
|
||||||
},
|
evalFn,
|
||||||
};
|
this,
|
||||||
|
undefined,
|
||||||
|
callFramesSnapshot?.frames ?? [],
|
||||||
|
callFrameRequests
|
||||||
|
);
|
||||||
|
|
||||||
DataExtractorApiImpl.lastVariablesInScope = variablesInScope;
|
DataExtractorApiImpl.lastContext = rootContext;
|
||||||
DataExtractorApiImpl.lastEvalFn = evalFn;
|
|
||||||
const value = valueFn();
|
const value = valueFn();
|
||||||
|
const extractions = this.getExtractions(value, rootContext);
|
||||||
|
DataExtractorApiImpl.lastContext = undefined;
|
||||||
|
|
||||||
for (const e of this.extractors.values()) {
|
const requestId =
|
||||||
e.getExtractions(value, extractionCollector, {
|
callFrameRequests.length === 0
|
||||||
evalFn,
|
? ""
|
||||||
variablesInScope,
|
: "" + cyrb53(JSON.stringify(callFrameRequests));
|
||||||
|
if ((callFramesSnapshot?.requestId ?? "") !== requestId) {
|
||||||
|
return this.toJson({
|
||||||
|
kind: "OutdatedCallFrameSnapshot",
|
||||||
|
callFramesRequest: {
|
||||||
|
requestId,
|
||||||
|
requestedCallFrames: callFrameRequests,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
DataExtractorApiImpl.lastVariablesInScope = undefined;
|
|
||||||
DataExtractorApiImpl.lastEvalFn = undefined;
|
|
||||||
|
|
||||||
extractions.sort((a, b) => b.priority - a.priority);
|
|
||||||
let usedExtraction = extractions[0];
|
let usedExtraction = extractions[0];
|
||||||
if (!usedExtraction) {
|
if (!usedExtraction) {
|
||||||
return this.toJson({ kind: "NoExtractors" } as DataResult);
|
return this.toJson({ kind: "NoExtractors" } as DataResult);
|
||||||
@@ -69,34 +85,210 @@ export class DataExtractorApiImpl implements DataExtractorApi {
|
|||||||
|
|
||||||
if (preferredDataExtractorId) {
|
if (preferredDataExtractorId) {
|
||||||
const preferred = extractions.find(
|
const preferred = extractions.find(
|
||||||
e => e.id === preferredDataExtractorId
|
(e) => e.id === preferredDataExtractorId
|
||||||
);
|
);
|
||||||
if (preferred) {
|
if (preferred) {
|
||||||
usedExtraction = preferred;
|
usedExtraction = preferred;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapExtractor(e: DataExtraction): DataExtractorInfo {
|
try {
|
||||||
return {
|
const data = usedExtraction.extractData();
|
||||||
id: e.id as any,
|
|
||||||
name: e.name,
|
return this.toJson({
|
||||||
priority: e.priority,
|
kind: "Data",
|
||||||
};
|
extractionResult: {
|
||||||
|
data,
|
||||||
|
usedExtractor: mapExtractor(usedExtraction),
|
||||||
|
availableExtractors: extractions.map(mapExtractor),
|
||||||
|
},
|
||||||
|
} as DataResult);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
return this.toJson({
|
||||||
|
kind: "Data",
|
||||||
|
extractionResult: {
|
||||||
|
data: visualizeError(e),
|
||||||
|
usedExtractor: mapExtractor(usedExtraction),
|
||||||
|
availableExtractors: extractions.map(mapExtractor),
|
||||||
|
},
|
||||||
|
} as DataResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExtractions(
|
||||||
|
value: unknown,
|
||||||
|
context: DataExtractorContext
|
||||||
|
): DataExtraction[] {
|
||||||
|
const extractions = new Array<DataExtraction>();
|
||||||
|
const extractors = new Array<DataExtractor>();
|
||||||
|
|
||||||
|
for (const fn of this.extractorSources.values()) {
|
||||||
|
try {
|
||||||
|
fn((extractor) => {
|
||||||
|
extractors.push(extractor);
|
||||||
|
}, helpers);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in data extractor source', fn, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = usedExtraction.extractData();
|
for (const extractor of [...this.extractors.values(), ...extractors]) {
|
||||||
return this.toJson({
|
if (extractor.dataCtor !== undefined) {
|
||||||
kind: "Data",
|
if (
|
||||||
extractionResult: {
|
typeof value !== "object" ||
|
||||||
data,
|
value === null ||
|
||||||
usedExtractor: mapExtractor(usedExtraction),
|
value.constructor.name !== extractor.dataCtor
|
||||||
availableExtractors: extractions.map(mapExtractor),
|
) {
|
||||||
},
|
continue;
|
||||||
} as DataResult);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
extractor.getExtractions(
|
||||||
|
value,
|
||||||
|
{
|
||||||
|
addExtraction(extraction) {
|
||||||
|
if (extraction.id === undefined) {
|
||||||
|
extraction.id = extractor.id;
|
||||||
|
}
|
||||||
|
if (extraction.name === undefined) {
|
||||||
|
extraction.name = extractor.id;
|
||||||
|
}
|
||||||
|
extractions.push(extraction);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
extractions.push({
|
||||||
|
id: extractor.id,
|
||||||
|
name: extractor.id,
|
||||||
|
priority: 0,
|
||||||
|
extractData() {
|
||||||
|
return visualizeError(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extractions.sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
|
return extractions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerDefaultExtractors(preferExisting: boolean = false): void {
|
public registerDefaultExtractors(preferExisting: boolean = false): void {
|
||||||
// TODO consider preferExisting
|
// TODO consider preferExisting
|
||||||
registerDefaultExtractors(this);
|
registerDefaultExtractors(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setDataExtractorFn(
|
||||||
|
id: string,
|
||||||
|
fn: LoadDataExtractorsFn | undefined
|
||||||
|
): void {
|
||||||
|
if (fn) {
|
||||||
|
this.extractorSources.set(id, fn);
|
||||||
|
} else {
|
||||||
|
this.extractorSources.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function visualizeError(e: unknown | Error): VisualizationData {
|
||||||
|
return helpers.asData({
|
||||||
|
kind: { text: true },
|
||||||
|
fileName: 'error.md',
|
||||||
|
text: `# Error while running data extractor\n\n${formatErrorStr(e)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorStr(e: unknown | Error): string {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
return `${e.message}\n\nStack:\n${e.stack}`;
|
||||||
|
}
|
||||||
|
return "" + e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExtractor(e: DataExtraction): DataExtractorInfo {
|
||||||
|
return {
|
||||||
|
id: e.id! as any,
|
||||||
|
name: e.name!,
|
||||||
|
priority: e.priority,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContextImpl implements DataExtractorContext {
|
||||||
|
constructor(
|
||||||
|
public readonly variablesInScope: Record<string, () => unknown>,
|
||||||
|
public readonly expression: string | undefined,
|
||||||
|
public readonly evalFn: <T>(expression: string) => T,
|
||||||
|
private readonly _api: DataExtractorApiImpl,
|
||||||
|
private readonly _parent: ContextImpl | undefined,
|
||||||
|
public readonly callFrameInfos: readonly (
|
||||||
|
| CallFrameInfo
|
||||||
|
| SkippedCallFrames
|
||||||
|
)[],
|
||||||
|
private readonly _callFrameRequests: CallFrameRequest[]
|
||||||
|
) { }
|
||||||
|
|
||||||
|
addCallFrameRequest(request: CallFrameRequest): void {
|
||||||
|
this._callFrameRequests.push(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
get _level(): number {
|
||||||
|
return this._parent ? this._parent._level + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extract(value: any): VisualizationData | undefined {
|
||||||
|
if (this._level > 10) {
|
||||||
|
throw new Error("extract() called too many times recursively");
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractions = this._api.getExtractions(
|
||||||
|
value,
|
||||||
|
new ContextImpl(
|
||||||
|
this.variablesInScope,
|
||||||
|
undefined,
|
||||||
|
this.evalFn,
|
||||||
|
this._api,
|
||||||
|
this,
|
||||||
|
this.callFrameInfos,
|
||||||
|
this._callFrameRequests
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (extractions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return extractions[0].extractData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStart(str: string, start: string): string {
|
||||||
|
if (str.startsWith(start)) {
|
||||||
|
return str.substr(start.length);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEnd(str: string, end: string): string {
|
||||||
|
if (str.endsWith(end)) {
|
||||||
|
return str.substr(0, str.length - end.length);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
|
||||||
|
function cyrb53(str: string, seed = 0) {
|
||||||
|
let h1 = 0xdeadbeef ^ seed,
|
||||||
|
h2 = 0x41c6ce57 ^ seed;
|
||||||
|
for (let i = 0, ch; i < str.length; i++) {
|
||||||
|
ch = str.charCodeAt(i);
|
||||||
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||||
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||||
|
}
|
||||||
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||||
|
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||||
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||||
|
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||||
|
|
||||||
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import {
|
|
||||||
DataExtractor,
|
|
||||||
ExtractionCollector,
|
|
||||||
DataExtractorContext,
|
|
||||||
LoadDataExtractorsFn,
|
|
||||||
} from "..";
|
|
||||||
import * as helpers from "../../helpers";
|
|
||||||
|
|
||||||
export class InjectedExtractor implements DataExtractor {
|
|
||||||
readonly id = "injected-extractor";
|
|
||||||
|
|
||||||
getExtractions(
|
|
||||||
data: unknown,
|
|
||||||
extractionCollector: ExtractionCollector,
|
|
||||||
context: DataExtractorContext
|
|
||||||
): void {
|
|
||||||
const key = "@hediet/debug-visualizer/injectedScripts";
|
|
||||||
const injectedExtractorsFns = Object.values(
|
|
||||||
(global as any)[key]
|
|
||||||
) as LoadDataExtractorsFn[];
|
|
||||||
|
|
||||||
const extractors = new Array<DataExtractor>();
|
|
||||||
|
|
||||||
for (const fn of injectedExtractorsFns) {
|
|
||||||
fn(extractor => {
|
|
||||||
extractors.push(extractor);
|
|
||||||
}, helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const extractor of extractors) {
|
|
||||||
extractor.getExtractions(data, extractionCollector, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { DataExtractor, ExtractionCollector, DataExtractorContext } from "..";
|
||||||
|
import { LineColumnRange } from "../../../CommonDataTypes";
|
||||||
|
import { asData, markedGrid, tryEval } from "../../helpers";
|
||||||
|
|
||||||
|
export class MarkedGridFromArrayExtractor implements DataExtractor {
|
||||||
|
readonly id = "markedGridFromArray";
|
||||||
|
getExtractions(
|
||||||
|
data: unknown,
|
||||||
|
collector: ExtractionCollector,
|
||||||
|
context: DataExtractorContext
|
||||||
|
): void {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data[0])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let markers: any;
|
||||||
|
if (data.length === 2 && typeof data[1] === "object") {
|
||||||
|
markers = data[1];
|
||||||
|
} else {
|
||||||
|
for (let i = 1; i < data.length; i++) {
|
||||||
|
if (typeof data[i] !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markers = tryEval(data.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
collector.addExtraction({
|
||||||
|
id: "markedGridFromArray",
|
||||||
|
name: "Marked Grid from Array",
|
||||||
|
priority: 1000,
|
||||||
|
extractData() {
|
||||||
|
return markedGrid(data[0], markers as any);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,12 @@ import {
|
|||||||
ExtractionCollector,
|
ExtractionCollector,
|
||||||
DataExtractorContext,
|
DataExtractorContext,
|
||||||
} from "../DataExtractorApi";
|
} from "../DataExtractorApi";
|
||||||
import { createGraph, CreateGraphEdge } from "../../helpers";
|
import {
|
||||||
|
createGraph,
|
||||||
|
CreateGraphEdge,
|
||||||
|
createGraphFromPointers,
|
||||||
|
} from "../../helpers";
|
||||||
|
import { GraphNode } from "../../..";
|
||||||
|
|
||||||
export class ObjectGraphExtractor implements DataExtractor {
|
export class ObjectGraphExtractor implements DataExtractor {
|
||||||
readonly id = "object-graph";
|
readonly id = "object-graph";
|
||||||
@@ -27,53 +32,83 @@ export class ObjectGraphExtractor implements DataExtractor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const infoSelector: (item: object) => {
|
||||||
|
id?: string | number;
|
||||||
|
edges: CreateGraphEdge<object>[];
|
||||||
|
} & Omit<GraphNode, "id"> = (item) => {
|
||||||
|
let label = "";
|
||||||
|
const edges = new Array<CreateGraphEdge<any>>();
|
||||||
|
if (item instanceof Set) {
|
||||||
|
label = "Set";
|
||||||
|
for (const value of item.values()) {
|
||||||
|
if (isObject(value)) {
|
||||||
|
edges.push({ label: "item", to: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (item instanceof Map) {
|
||||||
|
label = "Map";
|
||||||
|
for (const [key, value] of item.entries()) {
|
||||||
|
if (isObject(value)) {
|
||||||
|
edges.push({ label: key, to: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, val] of Object.entries(item)) {
|
||||||
|
if (isObject(val)) {
|
||||||
|
edges.push({ label: key, to: val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = item.constructor
|
||||||
|
? item.constructor.name
|
||||||
|
: "?";
|
||||||
|
label = className;
|
||||||
|
if (!Array.isArray(item)) {
|
||||||
|
try {
|
||||||
|
let toStrVal = item.toString();
|
||||||
|
if (toStrVal !== "[object Object]") {
|
||||||
|
if (toStrVal.length > 15) {
|
||||||
|
toStrVal += ` "${toStrVal.substr(0, 15)}..."`;
|
||||||
|
}
|
||||||
|
label += `${className}: ${toStrVal}`;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shape: "box",
|
||||||
|
edges,
|
||||||
|
color: item === data ? "lightblue" : undefined,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
collector.addExtraction({
|
collector.addExtraction({
|
||||||
id: "object-graph",
|
id: "object-graph",
|
||||||
name: "Object Graph",
|
name: "Object Graph",
|
||||||
priority: 99,
|
priority: 98,
|
||||||
extractData() {
|
extractData() {
|
||||||
return createGraph(
|
return createGraph([data], infoSelector, { maxSize: 50 });
|
||||||
[data],
|
|
||||||
item => {
|
|
||||||
let label = "";
|
|
||||||
const edges = new Array<CreateGraphEdge<any>>();
|
|
||||||
if (item instanceof Set) {
|
|
||||||
label = "Set";
|
|
||||||
for (const value of item.values()) {
|
|
||||||
if (isObject(value)) {
|
|
||||||
edges.push({ label: "item", to: value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (item instanceof Map) {
|
|
||||||
label = "Map";
|
|
||||||
for (const [key, value] of item.entries()) {
|
|
||||||
if (isObject(value)) {
|
|
||||||
edges.push({ label: key, to: value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [key, val] of Object.entries(item)) {
|
|
||||||
if (isObject(val)) {
|
|
||||||
edges.push({ label: key, to: val });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = item.constructor
|
|
||||||
? item.constructor.name
|
|
||||||
: "?";
|
|
||||||
label = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
shape: "box",
|
|
||||||
edges,
|
|
||||||
color: item === data ? "lightblue" : undefined,
|
|
||||||
label,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ maxSize: 50 }
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.constructor === Object &&
|
||||||
|
Object.values(data).every(
|
||||||
|
(v) => v === undefined || v === null || typeof v === "object"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
collector.addExtraction({
|
||||||
|
id: "object-graph-pointers",
|
||||||
|
name: "Object Graph With Pointers",
|
||||||
|
priority: 99,
|
||||||
|
extractData() {
|
||||||
|
return createGraphFromPointers(data as any, infoSelector, {
|
||||||
|
maxSize: 50,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
import { DataExtractor, DataExtractorContext, ExtractionCollector } from "../..";
|
import {
|
||||||
|
DataExtractor,
|
||||||
|
DataExtractorContext,
|
||||||
|
ExtractionCollector,
|
||||||
|
} from "../..";
|
||||||
|
|
||||||
export class StringDiffExtractor implements DataExtractor {
|
export class StringDiffExtractor implements DataExtractor {
|
||||||
public readonly id = "string-diff";
|
public readonly id = "string-diff";
|
||||||
|
|
||||||
public getExtractions(data: unknown,
|
public getExtractions(
|
||||||
|
data: unknown,
|
||||||
collector: ExtractionCollector,
|
collector: ExtractionCollector,
|
||||||
context: DataExtractorContext) {
|
context: DataExtractorContext
|
||||||
if (Array.isArray(data) && data.length === 2 && typeof data[0] === "string" && typeof data[1] === "string") {
|
) {
|
||||||
collector.addExtraction({
|
if (
|
||||||
id: "string-diff",
|
Array.isArray(data) &&
|
||||||
name: "String Diff",
|
data.length === 2 &&
|
||||||
priority: 1000,
|
typeof data[0] === "string" &&
|
||||||
extractData: () => ({
|
typeof data[1] === "string"
|
||||||
kind: { text: true },
|
) {
|
||||||
text: data[0],
|
collector.addExtraction({
|
||||||
otherText: data[1]
|
id: "string-diff",
|
||||||
})
|
name: "String Diff",
|
||||||
})
|
priority: 1300,
|
||||||
}
|
extractData: () => ({
|
||||||
}
|
kind: { text: true },
|
||||||
|
text: data[0],
|
||||||
|
otherText: data[1],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { DataExtractor, ExtractionCollector, DataExtractorContext } from "..";
|
||||||
|
import { LineColumnRange } from "../../../CommonDataTypes";
|
||||||
|
import { asData } from "../../helpers";
|
||||||
|
|
||||||
|
export class StringRangeExtractor implements DataExtractor {
|
||||||
|
readonly id = "stringRange";
|
||||||
|
getExtractions(
|
||||||
|
data: unknown,
|
||||||
|
collector: ExtractionCollector,
|
||||||
|
context: DataExtractorContext
|
||||||
|
): void {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof data[0] !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = data[0];
|
||||||
|
|
||||||
|
const decorations1: { range: LineColumnRange; label?: string }[] = [];
|
||||||
|
const decorations2: { range: LineColumnRange; label?: string }[] = [];
|
||||||
|
|
||||||
|
function offsetToLineColumn(offset: number) {
|
||||||
|
let line = 0;
|
||||||
|
let column = 0;
|
||||||
|
for (let idx = 0; idx < text.length; idx++) {
|
||||||
|
if (idx === offset) {
|
||||||
|
return { line, column };
|
||||||
|
}
|
||||||
|
if (text[idx] === "\n") {
|
||||||
|
line++;
|
||||||
|
column = 0;
|
||||||
|
} else {
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { line, column }; // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.length === 2 &&
|
||||||
|
typeof data[1] === "object" &&
|
||||||
|
!Array.isArray(data[1])
|
||||||
|
) {
|
||||||
|
data = [data[0], ...Object.values(data[1])] as [
|
||||||
|
number | [number, number]
|
||||||
|
][];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let item of (data as (number | [number, number])[]).slice(1)) {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
try {
|
||||||
|
item = context.evalFn(item);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item === "number") {
|
||||||
|
const pos = offsetToLineColumn(item);
|
||||||
|
decorations1.push({ range: { start: pos, end: pos } });
|
||||||
|
decorations2.push({
|
||||||
|
range: {
|
||||||
|
start: pos,
|
||||||
|
end: { line: pos.line, column: pos.column + 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
Array.isArray(item) &&
|
||||||
|
item.length === 2 &&
|
||||||
|
typeof item[0] === "number" &&
|
||||||
|
typeof item[1] === "number"
|
||||||
|
) {
|
||||||
|
decorations1.push({
|
||||||
|
range: {
|
||||||
|
start: offsetToLineColumn(item[0]),
|
||||||
|
end: offsetToLineColumn(item[1]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
decorations2.push(decorations1[decorations1.length - 1]);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collector.addExtraction({
|
||||||
|
priority: 1200,
|
||||||
|
id: "stringRange",
|
||||||
|
name: "String Range",
|
||||||
|
extractData() {
|
||||||
|
return asData({
|
||||||
|
kind: { text: true },
|
||||||
|
text,
|
||||||
|
decorations: decorations1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.addExtraction({
|
||||||
|
priority: 1000,
|
||||||
|
id: "stringRangeFullCharacters",
|
||||||
|
name: "String Range (Full Character)",
|
||||||
|
extractData() {
|
||||||
|
return asData({
|
||||||
|
kind: { text: true },
|
||||||
|
text,
|
||||||
|
decorations: decorations2,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,9 @@ export class TableDataExtractor implements DataExtractor {
|
|||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!data.every(d => typeof d === "object" && d)) {
|
if (
|
||||||
|
!data.every((d) => typeof d === "object" && d && !Array.isArray(d))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert<object[]>(data);
|
assert<object[]>(data);
|
||||||
@@ -44,7 +46,7 @@ export class TableDataExtractor implements DataExtractor {
|
|||||||
kind: {
|
kind: {
|
||||||
table: true,
|
table: true,
|
||||||
},
|
},
|
||||||
rows: data.map(d => ({ type: d.constructor.name, ...d })),
|
rows: data.map((d) => ({ type: d.constructor.name, ...d })),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { getDataExtractorApi } from "../injection";
|
|||||||
import { GridExtractor } from "./GridExtractor";
|
import { GridExtractor } from "./GridExtractor";
|
||||||
import { TableDataExtractor } from "./TableExtractor";
|
import { TableDataExtractor } from "./TableExtractor";
|
||||||
import { StringDiffExtractor } from "./StringDiffExtractor";
|
import { StringDiffExtractor } from "./StringDiffExtractor";
|
||||||
import { InjectedExtractor } from "./InjectedExtractor";
|
import { StringRangeExtractor } from "./StringRangeExtractor";
|
||||||
|
import { MarkedGridFromArrayExtractor } from "./MarkedGridExtractor";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data extractors should be registered by VS Code automatically.
|
* The default data extractors should be registered by VS Code automatically.
|
||||||
@@ -28,7 +29,8 @@ export function registerDefaultExtractors(
|
|||||||
new GridExtractor(),
|
new GridExtractor(),
|
||||||
new TableDataExtractor(),
|
new TableDataExtractor(),
|
||||||
new StringDiffExtractor(),
|
new StringDiffExtractor(),
|
||||||
new InjectedExtractor(),
|
new StringRangeExtractor(),
|
||||||
|
new MarkedGridFromArrayExtractor(),
|
||||||
]) {
|
]) {
|
||||||
api.registerExtractor(item);
|
api.registerExtractor(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { getGlobal } from "../../getGlobal";
|
||||||
|
import * as globalHelpers from "../global-helpers";
|
||||||
|
import * as helpers from "../helpers";
|
||||||
import { DataExtractorApi } from "./DataExtractorApi";
|
import { DataExtractorApi } from "./DataExtractorApi";
|
||||||
import { DataExtractorApiImpl } from "./DataExtractorApiImpl";
|
import { DataExtractorApiImpl } from "./DataExtractorApiImpl";
|
||||||
import * as helpers from "../helpers";
|
|
||||||
import * as globalHelpers from "../global-helpers";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns standalone JS code representing an expression that initializes the data extraction API.
|
* Returns standalone JS code representing an expression that initializes the data extraction API.
|
||||||
@@ -28,16 +29,11 @@ export function getExpressionForDataExtractorApi(): string {
|
|||||||
return `((${selfContainedGetInitializedDataExtractorApi.toString()})())`;
|
return `((${selfContainedGetInitializedDataExtractorApi.toString()})())`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpressionToDetectDataExtractorApiPresence(): string {
|
const apiKey = "@hediet/data-extractor-api/v3";
|
||||||
return `((${selfContainedIsDataExtractorApiInitialized.toString()})())`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = "@hediet/data-extractor-api/v2";
|
|
||||||
|
|
||||||
export function getDataExtractorApi(): DataExtractorApi {
|
export function getDataExtractorApi(): DataExtractorApi {
|
||||||
installHelpers();
|
installHelpers();
|
||||||
const globalObj =
|
const globalObj = getGlobal();
|
||||||
typeof window === "object" ? (window as any) : (global as any);
|
|
||||||
if (!globalObj[apiKey]) {
|
if (!globalObj[apiKey]) {
|
||||||
globalObj[apiKey] = new DataExtractorApiImpl();
|
globalObj[apiKey] = new DataExtractorApiImpl();
|
||||||
}
|
}
|
||||||
@@ -45,33 +41,41 @@ export function getDataExtractorApi(): DataExtractorApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This code is used to detect if the API has not been initialized yet.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
function selfContainedIsDataExtractorApiInitialized(): boolean {
|
export const ApiHasNotBeenInitializedCode = "EgH0cybXij1jYUozyakO" as const;
|
||||||
const globalObj =
|
|
||||||
typeof window === "object" ? (window as any) : (global as any);
|
|
||||||
const key: typeof apiKey = "@hediet/data-extractor-api/v2";
|
|
||||||
let api: DataExtractorApi | undefined = globalObj[key];
|
|
||||||
return api !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
|
function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
|
||||||
const globalObj =
|
function getGlobal(): any {
|
||||||
typeof window === "object" ? (window as any) : (global as any);
|
if (typeof globalThis === "object") {
|
||||||
const key: typeof apiKey = "@hediet/data-extractor-api/v2";
|
return globalThis;
|
||||||
let api: DataExtractorApi | undefined = globalObj[key];
|
} else if (typeof global === "object") {
|
||||||
if (!api) {
|
return global;
|
||||||
throw new Error(`Data Extractor API has not been initialized.`);
|
} else if (typeof window === "object") {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
throw new Error("No global available");
|
||||||
}
|
}
|
||||||
return api;
|
|
||||||
}
|
const globalObj = getGlobal();
|
||||||
|
const key: typeof apiKey = "@hediet/data-extractor-api/v3";
|
||||||
|
let api: DataExtractorApi | undefined = globalObj[key];
|
||||||
|
if (!api) {
|
||||||
|
const code: typeof ApiHasNotBeenInitializedCode =
|
||||||
|
"EgH0cybXij1jYUozyakO";
|
||||||
|
throw new Error(
|
||||||
|
`Data Extractor API has not been initialized. Code: ${code}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
export function installHelpers(): void {
|
export function installHelpers(): void {
|
||||||
const globalObj =
|
const globalObj = getGlobal();
|
||||||
typeof window === "object" ? (window as any) : (global as any);
|
|
||||||
// `hediet` as prefix to avoid name collision (I own `hediet.de`).
|
// `hediet` as prefix to avoid name collision (I own `hediet.de`).
|
||||||
globalObj["hedietDbgVis"] = { ...helpers, ...globalHelpers };
|
globalObj["hedietDbgVis"] = { ...helpers, ...globalHelpers };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export function cache<T>(
|
|||||||
let resultFn: () => any;
|
let resultFn: () => any;
|
||||||
let key: string;
|
let key: string;
|
||||||
if (typeof expression === "string") {
|
if (typeof expression === "string") {
|
||||||
const evalFn = DataExtractorApiImpl.lastEvalFn!;
|
const context = DataExtractorApiImpl.lastContext!;
|
||||||
resultFn = () => evalFn(expression);
|
resultFn = () => context.evalFn(expression);
|
||||||
key = JSON.stringify({ expr: expression, id });
|
key = JSON.stringify({ expr: expression, id });
|
||||||
} else {
|
} else {
|
||||||
resultFn = () => expression();
|
resultFn = () => expression();
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import { CreateGraphEdge, createGraph } from "./createGraph";
|
|||||||
*/
|
*/
|
||||||
export function createGraphFromPointers<T>(
|
export function createGraphFromPointers<T>(
|
||||||
roots: Record<string, T | undefined | null>,
|
roots: Record<string, T | undefined | null>,
|
||||||
infoSelector: (
|
infoSelector: (item: T) => {
|
||||||
item: T
|
|
||||||
) => {
|
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
edges: CreateGraphEdge<T>[];
|
edges: CreateGraphEdge<T>[];
|
||||||
} & Omit<GraphNode, "id">
|
} & Omit<GraphNode, "id">,
|
||||||
|
options: { maxSize?: number } = {}
|
||||||
): GraphVisualizationData {
|
): GraphVisualizationData {
|
||||||
const marker = {};
|
const marker = {};
|
||||||
|
|
||||||
@@ -28,18 +27,27 @@ export function createGraphFromPointers<T>(
|
|||||||
value,
|
value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return createGraph<T | Pointer>(items, item => {
|
return createGraph<T | Pointer>(
|
||||||
if ("marker" in item && item["marker"] === marker) {
|
items,
|
||||||
return {
|
(item) => {
|
||||||
id: "label____" + item.name,
|
if (
|
||||||
color: "orange",
|
typeof item === "object" &&
|
||||||
label: item.name,
|
item &&
|
||||||
edges: [{ to: item.value!, color: "orange", label: "" }].filter(
|
"marker" in item &&
|
||||||
t => !!t.to
|
item["marker"] === marker
|
||||||
),
|
) {
|
||||||
};
|
return {
|
||||||
} else {
|
id: "label____" + item.name,
|
||||||
return infoSelector(item as T);
|
color: "orange",
|
||||||
}
|
label: item.name,
|
||||||
});
|
edges: [
|
||||||
|
{ to: item.value!, color: "orange", label: "" },
|
||||||
|
].filter((t) => !!t.to),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return infoSelector(item as T);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,22 @@ import { DataExtractorApiImpl } from "../api";
|
|||||||
|
|
||||||
export function find(predicate: (obj: unknown) => boolean): unknown {
|
export function find(predicate: (obj: unknown) => boolean): unknown {
|
||||||
const processed = new Set();
|
const processed = new Set();
|
||||||
if (!DataExtractorApiImpl.lastVariablesInScope) {
|
if (!DataExtractorApiImpl.lastContext) {
|
||||||
throw new Error("No variables in scope!");
|
throw new Error("No data extractor context!");
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = Object.values(DataExtractorApiImpl.lastVariablesInScope);
|
const values = Object.values(
|
||||||
const queue = [...values];
|
DataExtractorApiImpl.lastContext.variablesInScope
|
||||||
|
);
|
||||||
|
const queue = [
|
||||||
|
...values.map((v) => {
|
||||||
|
try {
|
||||||
|
return v();
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
let i = 10000;
|
let i = 10000;
|
||||||
while (i > 0) {
|
while (i > 0) {
|
||||||
@@ -32,3 +42,95 @@ export function find(predicate: (obj: unknown) => boolean): unknown {
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findVar(
|
||||||
|
options: { nameSimilarTo?: string; ctor?: string },
|
||||||
|
predicate?: (value: any) => boolean
|
||||||
|
): unknown | undefined {
|
||||||
|
if (!DataExtractorApiImpl.lastContext) {
|
||||||
|
throw new Error("No data extractor context!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestValue = undefined;
|
||||||
|
let bestValueScore = undefined; // minimized
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(
|
||||||
|
DataExtractorApiImpl.lastContext.variablesInScope
|
||||||
|
)) {
|
||||||
|
const v = value();
|
||||||
|
if (options.ctor !== undefined) {
|
||||||
|
if (
|
||||||
|
typeof v !== "object" ||
|
||||||
|
!v ||
|
||||||
|
v.constructor.name !== options.ctor
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (predicate) {
|
||||||
|
if (!predicate(v)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let score = 0;
|
||||||
|
if (options.nameSimilarTo !== undefined) {
|
||||||
|
score += similarityScore(name, options.nameSimilarTo);
|
||||||
|
} else {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
if (bestValueScore === undefined || score < bestValueScore) {
|
||||||
|
bestValue = v;
|
||||||
|
bestValueScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function similarityScore(a: string, b: string): number {
|
||||||
|
const distance = levenshteinDistance(a, b);
|
||||||
|
|
||||||
|
const aSorted = a.split("").sort().join("");
|
||||||
|
const bSorted = b.split("").sort().join("");
|
||||||
|
const distance2 = levenshteinDistance(aSorted, bSorted);
|
||||||
|
|
||||||
|
return distance * 10 + distance2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function levenshteinDistance(a: string, b: string): number {
|
||||||
|
if (a.length === 0) return b.length;
|
||||||
|
if (b.length === 0) return a.length;
|
||||||
|
|
||||||
|
const matrix = [];
|
||||||
|
|
||||||
|
// increment along the first column of each row
|
||||||
|
let i;
|
||||||
|
for (i = 0; i <= b.length; i++) {
|
||||||
|
matrix[i] = [i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment each column in the first row
|
||||||
|
let j;
|
||||||
|
for (j = 0; j <= a.length; j++) {
|
||||||
|
matrix[0][j] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the rest of the matrix
|
||||||
|
for (i = 1; i <= b.length; i++) {
|
||||||
|
for (j = 1; j <= a.length; j++) {
|
||||||
|
if (b.charAt(i - 1) == a.charAt(j - 1)) {
|
||||||
|
matrix[i][j] = matrix[i - 1][j - 1];
|
||||||
|
} else {
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j - 1] + 1, // substitution
|
||||||
|
Math.min(
|
||||||
|
matrix[i][j - 1] + 1, // insertion
|
||||||
|
matrix[i - 1][j] + 1
|
||||||
|
)
|
||||||
|
); // deletion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix[b.length][a.length];
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,15 +4,28 @@ export function markedGrid(
|
|||||||
arr: unknown[],
|
arr: unknown[],
|
||||||
marked: Record<string, number | undefined>
|
marked: Record<string, number | undefined>
|
||||||
): GridVisualizationData {
|
): GridVisualizationData {
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
arr = [...(arr as any)];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: { grid: true },
|
kind: { grid: true },
|
||||||
rows: [{ columns: arr.map(d => ({ content: "" + d, tag: "" + d })) }],
|
rows: [
|
||||||
markers: Object.entries(marked)
|
{
|
||||||
.map(([key, val]) => ({
|
columns: arr.map((d) => ({
|
||||||
id: "" + key,
|
content: "" + d,
|
||||||
row: 0,
|
tag: "" + d,
|
||||||
column: val!,
|
})),
|
||||||
}))
|
},
|
||||||
.filter(c => c.column !== undefined),
|
],
|
||||||
|
markers: marked
|
||||||
|
? Object.entries(marked)
|
||||||
|
.map(([key, val]) => ({
|
||||||
|
id: "" + key,
|
||||||
|
row: 0,
|
||||||
|
column: val!,
|
||||||
|
}))
|
||||||
|
.filter((c) => c.column !== undefined)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,17 @@ export function tryEval(
|
|||||||
obj: Record<string, string> | string[] | string
|
obj: Record<string, string> | string[] | string
|
||||||
): Record<string, unknown> | unknown {
|
): Record<string, unknown> | unknown {
|
||||||
const result: Record<string, unknown> = {};
|
const result: Record<string, unknown> = {};
|
||||||
const evalFn = DataExtractorApiImpl.lastEvalFn!;
|
const context = DataExtractorApiImpl.lastContext!;
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
for (const val of obj) {
|
for (const val of obj) {
|
||||||
try {
|
try {
|
||||||
result[val] = evalFn(val);
|
result[val] = context.evalFn(val);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [key, val] of Object.entries(obj)) {
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
try {
|
try {
|
||||||
result[key] = evalFn(val);
|
result[key] = context.evalFn(val);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "ES2018",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
require("ts-node").register();
|
|
||||||
module.exports = require("./webpack.config.ts");
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as webpack from "webpack";
|
|
||||||
import path = require("path");
|
|
||||||
import { CleanWebpackPlugin } from "clean-webpack-plugin";
|
import { CleanWebpackPlugin } from "clean-webpack-plugin";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as webpack from "webpack";
|
||||||
|
|
||||||
const r = (file: string) => path.resolve(__dirname, file);
|
const r = (file: string) => path.resolve(__dirname, file);
|
||||||
|
|
||||||
|
|||||||
2920
data-extraction/yarn.lock
Normal file
2920
data-extraction/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,130 +23,37 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
register({
|
||||||
|
id: "binaryViewer",
|
||||||
|
getExtractions(data, collector, context) {
|
||||||
|
if (typeof data !== "number") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
register({
|
context.addCallFrameRequest({
|
||||||
id: "binaryViewer",
|
methodName: "Module._load",
|
||||||
getExtractions(data, collector, context) {
|
pathRegExp: ".*",
|
||||||
if (typeof data !== "number") {
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
collector.addExtraction({
|
context.addCallFrameRequest({
|
||||||
priority: 100,
|
methodName: "LinkedList.insertAt",
|
||||||
id: "binary",
|
pathRegExp: ".*",
|
||||||
name: "Bits of Integer",
|
});*/
|
||||||
extractData() {
|
|
||||||
return helpers.asData({
|
|
||||||
kind: { text: true },
|
|
||||||
text: data.toString(2),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
(register, helpers) => {
|
|
||||||
register({
|
|
||||||
id: "positionOrRangeInTextModel",
|
|
||||||
getExtractions(data, collector, context) {
|
|
||||||
collector.addExtraction({
|
collector.addExtraction({
|
||||||
priority: 100,
|
priority: 10000,
|
||||||
id: "foo",
|
id: "binary",
|
||||||
name: "Foo",
|
name: "Bits of Integer",
|
||||||
extractData() {
|
extractData() {
|
||||||
|
return context.extract(JSON.stringify(context.callFrameInfos, undefined, 4));
|
||||||
|
|
||||||
return helpers.asData({
|
return helpers.asData({
|
||||||
kind: { text: true },
|
kind: { text: true },
|
||||||
text: Object.keys(context.variablesInScope).join(", "),
|
text: data.toString(2),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
register({
|
|
||||||
id: "positionOrRangeInTextModel",
|
|
||||||
getExtractions(data, collector, context) {
|
|
||||||
/** @type {{ start: { line: number; column: number; }; end: { line: number; column: number; }; }} */
|
|
||||||
let range;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {any} */
|
|
||||||
const textModel = helpers.find(
|
|
||||||
x =>
|
|
||||||
typeof x === "object" &&
|
|
||||||
!!x &&
|
|
||||||
"constructor" in x &&
|
|
||||||
x.constructor.name === "TextModel"
|
|
||||||
);
|
|
||||||
|
|
||||||
collector.addExtraction({
|
|
||||||
id: "positionOrRangeInTextModel",
|
|
||||||
name: "Position/Range In TextModel",
|
|
||||||
extractData() {
|
|
||||||
return helpers.asData({
|
|
||||||
kind: { text: true },
|
|
||||||
text: textModel.getValue(),
|
|
||||||
decorations: [{ range }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
priority: 1000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {value is { startColumn: number; startLineNumber: number; endColumn: number; endLineNumber: number; }}
|
|
||||||
*/
|
|
||||||
function isRange(value) {
|
|
||||||
return (
|
|
||||||
typeof value === "object" &&
|
|
||||||
!!value &&
|
|
||||||
"startColumn" in value &&
|
|
||||||
"startLineNumber" in value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {value is { column: number; lineNumber: number }}
|
|
||||||
*/
|
|
||||||
function isPosition(value) {
|
|
||||||
return (
|
|
||||||
typeof value === "object" &&
|
|
||||||
!!value &&
|
|
||||||
"column" in value &&
|
|
||||||
"lineNumber" in value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
8
demos/js/src/demo_stack-frames.js
Normal file
8
demos/js/src/demo_stack-frames.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
function test() {
|
||||||
|
let i = 1;
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
test();
|
||||||
80
demos/lists/cpp/main.cpp
Normal file
80
demos/lists/cpp/main.cpp
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// A single node containing data and a raw pointer to the next node
|
||||||
|
template <typename T>
|
||||||
|
struct Node {
|
||||||
|
T data;
|
||||||
|
Node<T>* next;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The main wrapper for the linked list tracking the head node
|
||||||
|
template <typename T>
|
||||||
|
class LinkedList {
|
||||||
|
public:
|
||||||
|
LinkedList() : head_(nullptr) {}
|
||||||
|
|
||||||
|
~LinkedList() {
|
||||||
|
Node<T>* current = head_;
|
||||||
|
while (current) {
|
||||||
|
Node<T>* next = current->next;
|
||||||
|
delete current;
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw-pointer ownership can't be safely duplicated by a shallow copy
|
||||||
|
LinkedList(const LinkedList&) = delete;
|
||||||
|
LinkedList& operator=(const LinkedList&) = delete;
|
||||||
|
|
||||||
|
// Add a new element to the front of the list
|
||||||
|
void push_front(T data) {
|
||||||
|
head_ = new Node<T>{std::move(data), head_};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove and return the front element of the list
|
||||||
|
std::optional<T> pop_front() {
|
||||||
|
if (!head_) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
Node<T>* old_head = head_;
|
||||||
|
T data = std::move(old_head->data);
|
||||||
|
head_ = old_head->next;
|
||||||
|
delete old_head;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the front element without removing it
|
||||||
|
const T* peek_front() const {
|
||||||
|
return head_ ? &head_->data : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Node<T>* head_;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
LinkedList<int> list;
|
||||||
|
int count=0;
|
||||||
|
char * message1 = "hello world";
|
||||||
|
// Demonstrate pushing items
|
||||||
|
list.push_front(10);
|
||||||
|
count++;
|
||||||
|
list.push_front(20);
|
||||||
|
count++;
|
||||||
|
list.push_front(30);
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Demonstrate peeking at the top item
|
||||||
|
if (const int* top = list.peek_front()) {
|
||||||
|
std::cout << "Top element: " << *top << "\n"<<"count: "<<count<<"\n"; // Output: 30
|
||||||
|
}
|
||||||
|
std::cout<<message1<<"\n";
|
||||||
|
|
||||||
|
// Demonstrate popping items
|
||||||
|
while (auto value = list.pop_front()) {
|
||||||
|
std::cout << "Popped: " << *value << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
16
demos/lists/python/.vscode/launch.json
vendored
Normal file
16
demos/lists/python/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Python Debugger: Current File",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
63
demos/lists/python/main.py
Normal file
63
demos/lists/python/main.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Generic, Optional, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
# A single node containing data and a reference to the next node
|
||||||
|
@dataclass
|
||||||
|
class Node(Generic[T]):
|
||||||
|
data: T
|
||||||
|
next: Optional["Node[T]"]
|
||||||
|
|
||||||
|
|
||||||
|
# The main wrapper for the linked list tracking the head node
|
||||||
|
class LinkedList(Generic[T]):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._head: Optional[Node[T]] = None
|
||||||
|
|
||||||
|
# Add a new element to the front of the list
|
||||||
|
def push_front(self, data: T) -> None:
|
||||||
|
self._head = Node(data, self._head)
|
||||||
|
|
||||||
|
# Remove and return the front element of the list
|
||||||
|
def pop_front(self) -> Optional[T]:
|
||||||
|
if self._head is None:
|
||||||
|
return None
|
||||||
|
data = self._head.data
|
||||||
|
self._head = self._head.next
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Read the front element without removing it
|
||||||
|
def peek_front(self) -> Optional[T]:
|
||||||
|
return self._head.data if self._head else None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
lst: LinkedList[int] = LinkedList()
|
||||||
|
count = 0
|
||||||
|
message1 = "hello world"
|
||||||
|
|
||||||
|
# Demonstrate pushing items
|
||||||
|
lst.push_front(10)
|
||||||
|
count += 1
|
||||||
|
lst.push_front(20)
|
||||||
|
count += 1
|
||||||
|
lst.push_front(30)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
# Demonstrate peeking at the top item
|
||||||
|
top = lst.peek_front()
|
||||||
|
if top is not None:
|
||||||
|
print(f"Top element: {top}\ncount: {count}") # Output: 30
|
||||||
|
print(message1)
|
||||||
|
|
||||||
|
# Demonstrate popping items
|
||||||
|
while (value := lst.pop_front()) is not None:
|
||||||
|
print(f"Popped: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
21
demos/lists/rust/.vscode/launch.json
vendored
Normal file
21
demos/lists/rust/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "cppdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug list_example",
|
||||||
|
"program": "${workspaceFolder}/target/debug/list_example",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"preLaunchTask": "cargo build",
|
||||||
|
"MIMode": "gdb",
|
||||||
|
"miDebuggerPath": "/usr/bin/rust-gdb",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"externalConsole": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
demos/lists/rust/.vscode/tasks.json
vendored
Normal file
17
demos/lists/rust/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"command": "build",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"label": "cargo build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
demos/lists/rust/Cargo.lock
generated
Normal file
7
demos/lists/rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "list_example"
|
||||||
|
version = "0.1.0"
|
||||||
6
demos/lists/rust/Cargo.toml
Normal file
6
demos/lists/rust/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "list_example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
65
demos/lists/rust/src/main.rs
Normal file
65
demos/lists/rust/src/main.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// A single node containing data and an optional pointer to the next node
|
||||||
|
struct Node<T> {
|
||||||
|
data: T,
|
||||||
|
next: Option<Box<Node<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main wrapper for the linked list tracking the head node
|
||||||
|
pub struct LinkedList<T> {
|
||||||
|
head: Option<Box<Node<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> LinkedList<T> {
|
||||||
|
// Create an empty linked list
|
||||||
|
pub fn new() -> Self {
|
||||||
|
LinkedList { head: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new element to the front of the list
|
||||||
|
pub fn push_front(&mut self, data: T) {
|
||||||
|
let new_node = Box::new(Node {
|
||||||
|
data,
|
||||||
|
// .take() temporarily leaves None in self.head and extracts the value
|
||||||
|
next: self.head.take(),
|
||||||
|
});
|
||||||
|
self.head = Some(new_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove and return the front element of the list
|
||||||
|
pub fn pop_front(&mut self) -> Option<T> {
|
||||||
|
self.head.take().map(|node| {
|
||||||
|
self.head = node.next;
|
||||||
|
node.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the front element without removing it
|
||||||
|
pub fn peek_front(&self) -> Option<&T> {
|
||||||
|
self.head.as_ref().map(|node| &node.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut list = LinkedList::new();
|
||||||
|
let mut count = 0;
|
||||||
|
let message1 = "hello world";
|
||||||
|
|
||||||
|
// Demonstrate pushing items
|
||||||
|
list.push_front(10);
|
||||||
|
count += 1;
|
||||||
|
list.push_front(20);
|
||||||
|
count += 1;
|
||||||
|
list.push_front(30);
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
// Demonstrate peeking at the top item
|
||||||
|
if let Some(top) = list.peek_front() {
|
||||||
|
println!("Top element: {}\ncount: {}", top, count); // Output: 30
|
||||||
|
}
|
||||||
|
println!("{}", message1);
|
||||||
|
|
||||||
|
// Demonstrate popping items
|
||||||
|
while let Some(value) = list.pop_front() {
|
||||||
|
println!("Popped: {}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
demos/python/Person.py
Normal file
7
demos/python/Person.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class Person:
|
||||||
|
def __init__(self, name, parents=None) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.parents = [] if parents is None else parents
|
||||||
|
|
||||||
|
def addParent(self, parent: "Person"):
|
||||||
|
self.parents.append(parent)
|
||||||
44
demos/python/debugvisualizer.py
Normal file
44
demos/python/debugvisualizer.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from Person import Person
|
||||||
|
import json
|
||||||
|
from vscodedebugvisualizer import globalVisualizationFactory
|
||||||
|
|
||||||
|
|
||||||
|
class PersonVisualizer:
|
||||||
|
def checkType(self, t):
|
||||||
|
return isinstance(t, Person)
|
||||||
|
|
||||||
|
def visualizePerson(self, person: Person, nodes=[], edges=[]):
|
||||||
|
if person.name in [n["id"] for n in nodes]:
|
||||||
|
return nodes, edges
|
||||||
|
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"id": person.name,
|
||||||
|
"label": person.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for p in person.parents:
|
||||||
|
nodes, edges = self.visualizePerson(p, nodes, edges)
|
||||||
|
edges.append(
|
||||||
|
{
|
||||||
|
"from": p.name,
|
||||||
|
"to": person.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return nodes, edges
|
||||||
|
|
||||||
|
def visualize(self, person: Person):
|
||||||
|
jsonDict = {
|
||||||
|
"kind": {"graph": True},
|
||||||
|
"nodes": [],
|
||||||
|
"edges": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.visualizePerson(person, jsonDict["nodes"], jsonDict["edges"])
|
||||||
|
|
||||||
|
return json.dumps(jsonDict)
|
||||||
|
|
||||||
|
|
||||||
|
globalVisualizationFactory.addVisualizer(PersonVisualizer())
|
||||||
123
demos/python/demo.py
Normal file
123
demos/python/demo.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# put "x" in the Debug Visualizer and use step by step
|
||||||
|
# debugging to see the diffrent types of data visualization
|
||||||
|
|
||||||
|
|
||||||
|
x = 5
|
||||||
|
x = "asdf"
|
||||||
|
x = 5.5
|
||||||
|
x = [1, 2, 3, 4, 5, 6, "a", "b"]
|
||||||
|
x = ["b", "a", "c", "d", "e"]
|
||||||
|
x.sort()
|
||||||
|
x = {
|
||||||
|
"asdf1": "dasdf",
|
||||||
|
"asdf2": "dasdf",
|
||||||
|
"asdf3": {"foo": "bar"},
|
||||||
|
}
|
||||||
|
|
||||||
|
x = [1, 2, 3, 4, 5, 6]
|
||||||
|
# histogram
|
||||||
|
x = np.concatenate([np.arange(i) for i in range(9)])
|
||||||
|
x = x.reshape(2, -1)
|
||||||
|
|
||||||
|
|
||||||
|
# one dimension
|
||||||
|
x = np.arange(100)
|
||||||
|
x = np.linspace(-np.pi, np.pi, 100)
|
||||||
|
x = np.sin(x)
|
||||||
|
|
||||||
|
# 2 dimension
|
||||||
|
x = x.reshape(5, 20)
|
||||||
|
# 2 dimension transpose
|
||||||
|
x = x.transpose()
|
||||||
|
x = x.transpose()
|
||||||
|
|
||||||
|
# 3 dimensions
|
||||||
|
x = x.reshape(2, 5, 10)
|
||||||
|
|
||||||
|
# 4 dimensions
|
||||||
|
x = x.reshape(2, 5, 2, 5)
|
||||||
|
|
||||||
|
# big number
|
||||||
|
x = np.empty(2 ** 24)
|
||||||
|
x[0:1000000] = 1
|
||||||
|
x[1000000:10000000] = 5
|
||||||
|
|
||||||
|
# pyTorch
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
|
||||||
|
x = np.concatenate([np.arange(i) for i in range(9)])
|
||||||
|
x = x.reshape(2, -1)
|
||||||
|
x = torch.Tensor(x)
|
||||||
|
pass
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# tensorflow
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
x = np.concatenate([np.arange(i) for i in range(9)])
|
||||||
|
x = x.reshape(2, -1)
|
||||||
|
x = tf.convert_to_tensor(x)
|
||||||
|
pass
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# pandas
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
import plotly.express as px
|
||||||
|
|
||||||
|
x = px.data.gapminder().query("year == 2007")
|
||||||
|
pass
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# custom visualizer defined in ./debugvisualizer.py (this file is included automatically)
|
||||||
|
|
||||||
|
from Person import Person
|
||||||
|
|
||||||
|
x = Person("Aria")
|
||||||
|
parent1 = Person("Eduart")
|
||||||
|
parent2 = Person("Catelyn")
|
||||||
|
x.addParent(parent1)
|
||||||
|
x.addParent(parent2)
|
||||||
|
parent1.addParent(Person("Benjen"))
|
||||||
|
|
||||||
|
# direct debug-visualizer json as dict with property "kind"
|
||||||
|
|
||||||
|
x = {
|
||||||
|
"kind": {"dotGraph": True},
|
||||||
|
"text": '\ndigraph G {\n subgraph cluster_0 {\n style=filled;\n color=lightgrey;\n node [style=filled,color=white];\n a0 -> a1 -> a2 -> a3;\n label = "process #1";\n }\n \n subgraph cluster_1 {\n node [style=filled];\n b0 -> b1 -> b2 -> b3;\n label = "process #2";\n color=blue\n }\n start -> a0;\n start -> b0;\n a1 -> b3;\n b2 -> a3;\n a3 -> a0;\n a3 -> end;\n b3 -> end;\n \n start [shape=Mdiamond];\n end [shape=Msquare];\n}\n',
|
||||||
|
}
|
||||||
|
|
||||||
|
x = {
|
||||||
|
"kind": {"plotly": True},
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"mode": "lines",
|
||||||
|
"type": "scatter",
|
||||||
|
"x": ["A", "B", "C"],
|
||||||
|
"xaxis": "x",
|
||||||
|
"y": [4488916, 3918072, 3892124],
|
||||||
|
"yaxis": "y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": {"values": [["A", "B", "C"], [341319, 281489, 294786], [4488916, 3918072, 3892124]]},
|
||||||
|
"domain": {"x": [0.0, 1.0], "y": [0.0, 0.60]},
|
||||||
|
"header": {"align": "left", "font": {"size": 10}, "values": ["Date", "Number", "Output"]},
|
||||||
|
"type": "table",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"layout": {"xaxis": {"anchor": "y", "domain": [0.0, 1.0]}, "yaxis": {"anchor": "x", "domain": [0.75, 1.0]}},
|
||||||
|
}
|
||||||
|
pass
|
||||||
@@ -23,10 +23,9 @@ for i in range(2,100):
|
|||||||
# connects the node to a random edge
|
# connects the node to a random edge
|
||||||
targetId = str(randint(1, i - 1))
|
targetId = str(randint(1, i - 1))
|
||||||
graph["edges"].append({"from": id, "to": targetId})
|
graph["edges"].append({"from": id, "to": targetId})
|
||||||
json_graph = dumps(graph)
|
|
||||||
print("i is " + str(i))
|
print("i is " + str(i))
|
||||||
# try setting a breakpoint right above
|
# try setting a breakpoint right above
|
||||||
# then put json_graph into the visualization console and press enter
|
# then put graph into the visualization console and press enter
|
||||||
# when you step through the code each time you hit the breakpoint
|
# when you step through the code each time you hit the breakpoint
|
||||||
# the graph should automatically refresh!
|
# the graph should automatically refresh!
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Python demo for sorting using VS Code Debug Visualizer."""
|
"""Python demo for sorting using VS Code Debug Visualizer."""
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def serialize(arr):
|
def serialize(arr):
|
||||||
@@ -14,7 +13,7 @@ def serialize(arr):
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return json.dumps(formatted)
|
return formatted
|
||||||
|
|
||||||
|
|
||||||
arr = [6, 9, 3, 12, 1, 11, 5, 13, 8, 14, 2, 4, 10, 0, 7]
|
arr = [6, 9, 3, 12, 1, 11, 5, 13, 8, 14, 2, 4, 10, 0, 7]
|
||||||
|
|||||||
7
demos/ruby/README.md
Normal file
7
demos/ruby/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Ruby Demos
|
||||||
|
|
||||||
|
To visualize Ruby objects, you need to install [debugvisualizer.gem](https://github.com/ono-max/debugvisualizer).
|
||||||
|
|
||||||
|
Install the gem by executing:
|
||||||
|
|
||||||
|
$ gem install debugvisualizer
|
||||||
18
demos/ruby/src/demo_custom_visualizer.rb
Normal file
18
demos/ruby/src/demo_custom_visualizer.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
require 'debugvisualizer'
|
||||||
|
|
||||||
|
class Foo; end
|
||||||
|
|
||||||
|
DebugVisualizer.register Foo do |data|
|
||||||
|
{
|
||||||
|
id: "my_visualizer",
|
||||||
|
name: "My Visualizer",
|
||||||
|
data: {
|
||||||
|
kind: { text: true },
|
||||||
|
text: "Foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
f = Foo.new
|
||||||
|
# visualize `f` here!
|
||||||
|
binding.break
|
||||||
12
demos/ruby/src/demo_random_walks.rb
Normal file
12
demos/ruby/src/demo_random_walks.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
data = []
|
||||||
|
curVal = 0
|
||||||
|
|
||||||
|
100.times{
|
||||||
|
100.times{
|
||||||
|
delta = rand > 0.5 ? 1 : -1
|
||||||
|
data << curVal
|
||||||
|
curVal += delta
|
||||||
|
# visualize `data` here and press F5(or Continue button) over and over!
|
||||||
|
binding.break
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/custom-scripts.gif
Normal file
BIN
docs/custom-scripts.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@@ -1,5 +1,20 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 2.6.0
|
||||||
|
|
||||||
|
- Improved JS data extractors
|
||||||
|
- Uses the active stack frame (instead of the top-most) for evaluating expressions
|
||||||
|
|
||||||
|
## 2.4.0
|
||||||
|
|
||||||
|
- [Ruby support](https://github.com/hediet/vscode-debug-visualizer/pull/159)
|
||||||
|
- New setting `debugVisualizer.customVisualizerScriptPaths` to support custom visualizations
|
||||||
|
- Added JS data extractors
|
||||||
|
|
||||||
|
## 2.3.0
|
||||||
|
|
||||||
|
- [Improves support for python](https://github.com/hediet/vscode-debug-visualizer/pull/122)
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
||||||
- Adds setting `debugVisualizer.js.customScriptPaths` to inject custom scripts when debugging JavaScript. Such scripts are reloaded automatically when changed.
|
- Adds setting `debugVisualizer.js.customScriptPaths` to inject custom scripts when debugging JavaScript. Such scripts are reloaded automatically when changed.
|
||||||
|
|||||||
674
extension/LICENSE.md
Normal file
674
extension/LICENSE.md
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and`show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
@@ -25,6 +25,7 @@ See [demos](../demos/) for demos. These languages and debuggers are verified to
|
|||||||
- C++ using `cppdbg` debug adapter: [✅ Basic Support](../demos/cpp)
|
- C++ using `cppdbg` debug adapter: [✅ Basic Support](../demos/cpp)
|
||||||
- Swift using `lldb` debug adapter: [✅ Basic Support](../demos/swift)
|
- Swift using `lldb` debug adapter: [✅ Basic Support](../demos/swift)
|
||||||
- Rust using `lldb` debug adapter: [✅ Basic Support](../demos/rust)
|
- Rust using `lldb` debug adapter: [✅ Basic Support](../demos/rust)
|
||||||
|
- Ruby using `rdbg` debug adapter: [✅ Basic Support](../demos/ruby)
|
||||||
|
|
||||||
All other languages and debuggers might work too.
|
All other languages and debuggers might work too.
|
||||||
For languages with _Basic Support_, only JSON strings can be visualized - you must implement some logic that builds this JSON for your data structure!
|
For languages with _Basic Support_, only JSON strings can be visualized - you must implement some logic that builds this JSON for your data structure!
|
||||||
@@ -112,68 +113,71 @@ Also, a global object of name `hedietDbgVis` with helper functions is injected.
|
|||||||
|
|
||||||
This extension provides these configuration options:
|
This extension provides these configuration options:
|
||||||
|
|
||||||
- `debugVisualizer.debugAdapterConfigurations`
|
### **debugVisualizer.debugAdapterConfigurations**
|
||||||
|
|
||||||
Allows to set expression templates for specific debug adapter types.
|
Allows to set expression templates for specific debug adapter types.
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"debugVisualizer.debugAdapterConfigurations": {
|
"debugVisualizer.debugAdapterConfigurations": {
|
||||||
"lldb": {
|
"lldb": {
|
||||||
"expressionTemplate": "script to_json(\"${expr}\")",
|
"expressionTemplate": "script to_json(\"${expr}\")",
|
||||||
"context": "repl"
|
"context": "repl"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Configurations here overwrite the built-in support for the corresponding debug adapter type.
|
Configurations here overwrite the built-in support for the corresponding debug adapter type.
|
||||||
|
|
||||||
- `debugVisualizer.useChromeKioskMode`
|
### **debugVisualizer.useChromeKioskMode**
|
||||||
|
|
||||||
Specifies whether to pop out Debug Visualization Views with Chrome in Kiosk Mode. Uses the default browser otherwise or if Chrome is not found. Defaults to `true`.
|
Specifies whether to pop out Debug Visualization Views with Chrome in Kiosk Mode. Uses the default browser otherwise or if Chrome is not found. Defaults to `true`.
|
||||||
|
|
||||||
- `debugVisualizer.js.customScriptPaths`
|
### **debugVisualizer.js.customScriptPaths**
|
||||||
|
|
||||||
Specifies a list of JavaScript files that are injected into the debugee when debugging JavaScript.
|
Specifies a list of JavaScript files that are injected into the debugee when debugging JavaScript.
|
||||||
Each script must assign `module.exports` with a function of type `import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn`.
|
Each script must assign `module.exports` with a function of type `import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn`.
|
||||||
Paths must be absolute and can use the variable `${workspaceFolder}`.
|
Paths must be absolute and can use the variable `${workspaceFolder}`.
|
||||||
Scripts are automatically reloaded when they are changed.
|
Scripts are automatically reloaded when they are changed.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// @ts-check
|
// @ts-check
|
||||||
/**
|
/**
|
||||||
* @type {import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn}
|
* @type {import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn}
|
||||||
*/
|
*/
|
||||||
module.exports = (register, helpers) => {
|
module.exports = (register, helpers) => {
|
||||||
register({
|
register({
|
||||||
id: "map",
|
id: "map",
|
||||||
getExtractions(data, collector, context) {
|
getExtractions(data, collector, context) {
|
||||||
if (!(data instanceof Map)) {
|
if (!(data instanceof Map)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
collector.addExtraction({
|
collector.addExtraction({
|
||||||
priority: 1000,
|
priority: 1000,
|
||||||
id: "map",
|
id: "map",
|
||||||
name: "Map",
|
name: "Map",
|
||||||
extractData() {
|
extractData() {
|
||||||
return helpers.asData({
|
return helpers.asData({
|
||||||
kind: { table: true },
|
kind: { table: true },
|
||||||
rows: [...data].map(([k, v]) => ({
|
rows: [...data].map(([k, v]) => ({
|
||||||
key: k,
|
key: k,
|
||||||
value: v,
|
value: v,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
Use the `helpers` object to access some magic functions.
|
||||||
|
`helpers.find(val => ...)` can be used to find an object that is reachable by any variable in scope.
|
||||||
|
|
||||||
# See Also
|
# See Also
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
"displayName": "Debug Visualizer",
|
"displayName": "Debug Visualizer",
|
||||||
"description": "A visual watch window that lets you visualize your data structures while debugging.",
|
"description": "A visual watch window that lets you visualize your data structures while debugging.",
|
||||||
"icon": "docs/logo.drawio.png",
|
"icon": "docs/logo.drawio.png",
|
||||||
"version": "2.2.1",
|
"version": "3.0.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.46.0"
|
"vscode": "^1.84.0"
|
||||||
},
|
},
|
||||||
"publisher": "hediet",
|
"publisher": "ai-coder",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"debugger",
|
"debugger",
|
||||||
"debugging",
|
"debugging",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"visualization"
|
"visualization"
|
||||||
],
|
],
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/hediet/vscode-debug-visualizer/issues"
|
"url": "https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer/issues"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"email": "henning.dieterichs@live.de",
|
"email": "henning.dieterichs@live.de",
|
||||||
@@ -30,14 +30,12 @@
|
|||||||
"readme": "./README.md",
|
"readme": "./README.md",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/hediet/vscode-debug-visualizer.git"
|
"url": "https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onCommand:vscode-debug-visualizer.new-visualizer",
|
|
||||||
"onCommand:vscode-debug-visualizer.visualizer-set-expression",
|
|
||||||
"onDebug",
|
"onDebug",
|
||||||
"onWebviewPanel:debugVisualizer"
|
"onWebviewPanel:debugVisualizer"
|
||||||
],
|
],
|
||||||
@@ -70,10 +68,18 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"debugVisualizer.js.customScriptPaths": {
|
"debugVisualizer.js.customScriptPaths": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Defines JavaScript sources that are injected into the debuggee.",
|
"description": "Defines JavaScript sources that are injected into the debuggee to provide custom data extractors.",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The file path to a custom js script. Must be absolute. You can use ${workspaceFolder}."
|
"description": "The file path to a custom js script. Must be absolute. You can use ${workspaceFolder}. Must assign `module.exports` an object of type `import('@hediet/debug-visualizer-data-extraction').LoadDataExtractorsFn`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debugVisualizer.customVisualizerScriptPaths": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Defines JavaScript sources that are injected into the visualizer to provide custom visualizations.",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The file path to a custom js script. Must be absolute. You can use ${workspaceFolder}. Must assign `module.exports` an object of type `import('@hediet/visualization-core').RegisterVisualizerFn`."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debugVisualizer.useChromeKioskMode": {
|
"debugVisualizer.useChromeKioskMode": {
|
||||||
@@ -97,6 +103,15 @@
|
|||||||
"repl"
|
"repl"
|
||||||
],
|
],
|
||||||
"description": "The context to use for evaluating the expression."
|
"description": "The context to use for evaluating the expression."
|
||||||
|
},
|
||||||
|
"nodeTableGraph": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Use the built-in node-table graph visualization for this debug adapter type, walking the expression via the standard evaluate/variables/scopes requests instead of expressionTemplate. Enabled by default for every adapter type except ones with a more specific built-in engine (node/chrome/pwa-* JS adapters, rdbg); set to false to fall back to expressionTemplate for a given adapter type, or true to opt a JS/Ruby adapter back in."
|
||||||
|
},
|
||||||
|
"setupExpression": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Commands run once per debug session (context \"repl\") before the node-table graph walk -- e.g. gdb's pretty-printing/print-elements/print-characters options."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,9 +120,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pub": "vsce publish --yarn --baseImagesUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension --baseContentUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension",
|
"pub": "vsce publish --packagePath ./dist/extension.vsix",
|
||||||
"package": "vsce package --yarn",
|
"package": "vsce package --no-dependencies -o ./dist/extension.vsix --yarn --baseImagesUrl https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer/raw/master/extension --baseContentUrl https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer/raw/master/extension",
|
||||||
"vscode:prepublish": "yarn build",
|
"vscode:prepublish": "",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"dev": "shx rm -rf dist && tsc -watch -p ./"
|
"dev": "shx rm -rf dist && tsc -watch -p ./"
|
||||||
},
|
},
|
||||||
@@ -125,18 +140,18 @@
|
|||||||
"mobx-utils": "^5.6.1"
|
"mobx-utils": "^5.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"@types/copy-webpack-plugin": "^5.0.0",
|
"@types/copy-webpack-plugin": "^10.1.0",
|
||||||
"@types/express": "^4.17.2",
|
"@types/express": "^4.17.2",
|
||||||
"@types/serve-static": "^1.13.3",
|
"@types/serve-static": "^1.13.3",
|
||||||
"@types/node": "^13.7.4",
|
"@types/node": "^13.7.4",
|
||||||
"@types/vscode": "1.46.0",
|
"@types/vscode": "^1.84.0",
|
||||||
"tslint": "^6.0.0",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^3.8.2",
|
"typescript": "^5.1.6",
|
||||||
"webpack": "^4.41.6",
|
"webpack": "^5.88.1",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^5.1.4",
|
||||||
"ts-loader": "^6.2.1",
|
"ts-loader": "^9.4.4",
|
||||||
"shx": "^0.3.2",
|
"shx": "^0.3.2",
|
||||||
"vsce": "^1.93.0"
|
"vsce": "2.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,30 @@ export class Config {
|
|||||||
);
|
);
|
||||||
|
|
||||||
public get customScriptPaths(): string[] {
|
public get customScriptPaths(): string[] {
|
||||||
return this._customScriptPaths.get().map(p => {
|
return this._customScriptPaths.get().map((p) => {
|
||||||
|
const tpl = new SimpleTemplate(p);
|
||||||
|
return tpl.render({
|
||||||
|
workspaceFolder: () => {
|
||||||
|
const workspaceFolder = (workspace.workspaceFolders ||
|
||||||
|
[])[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot get workspace folder - '${p}' cannot be evaluated!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return workspaceFolder.uri.fsPath;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _customVisualizerScriptPaths = new VsCodeSetting(
|
||||||
|
"debugVisualizer.customVisualizerScriptPaths",
|
||||||
|
{ serializer: serializerWithDefault<string[]>([]) }
|
||||||
|
);
|
||||||
|
|
||||||
|
public get customVisualizerScriptPaths(): string[] {
|
||||||
|
return this._customVisualizerScriptPaths.get().map((p) => {
|
||||||
const tpl = new SimpleTemplate(p);
|
const tpl = new SimpleTemplate(p);
|
||||||
return tpl.render({
|
return tpl.render({
|
||||||
workspaceFolder: () => {
|
workspaceFolder: () => {
|
||||||
@@ -64,21 +87,65 @@ export class Config {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adapter types that already have a dedicated, richer visualization
|
||||||
|
// engine (JsEvaluationEngine injects real typed data-extractors into the
|
||||||
|
// debuggee; RbEvaluationEngine similarly) -- the generic node-table graph
|
||||||
|
// defaults to *on* for every other adapter type (see getDebugAdapterConfig
|
||||||
|
// below) so it works out of the box across languages with no workspace
|
||||||
|
// config required, but shouldn't shadow those two's better-than-generic
|
||||||
|
// support unless a user explicitly opts back in via
|
||||||
|
// `debugVisualizer.debugAdapterConfigurations`.
|
||||||
|
private static readonly nodeTableGraphExcludedAdapters = new Set([
|
||||||
|
"node",
|
||||||
|
"node2",
|
||||||
|
"extensionHost",
|
||||||
|
"chrome",
|
||||||
|
"pwa-chrome",
|
||||||
|
"pwa-node",
|
||||||
|
"pwa-extensionHost",
|
||||||
|
"node-terminal",
|
||||||
|
"pwa-msedge",
|
||||||
|
"rdbg",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Built-in per-adapter extras beyond the nodeTableGraph on/off default
|
||||||
|
// above -- currently just gdb's display-option setup commands, so the
|
||||||
|
// node-table graph's pretty-printed/full-length output works with no
|
||||||
|
// workspace config required. The user's own
|
||||||
|
// `debugVisualizer.debugAdapterConfigurations` setting still wins
|
||||||
|
// field-by-field (see getDebugAdapterConfig's merge below).
|
||||||
|
private static readonly builtInDebugAdapterConfigs: DebugAdapterConfigs = {
|
||||||
|
cppdbg: {
|
||||||
|
setupExpression: [
|
||||||
|
"-enable-pretty-printing",
|
||||||
|
"-gdb-set print elements 0",
|
||||||
|
"-gdb-set print characters 0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
public getDebugAdapterConfig(
|
public getDebugAdapterConfig(
|
||||||
debugAdapterType: string
|
debugAdapterType: string
|
||||||
): DebugAdapterConfig | undefined {
|
): DebugAdapterConfig | undefined {
|
||||||
const c = this._debugAdapterConfigs.get()[debugAdapterType];
|
const builtIn = Config.builtInDebugAdapterConfigs[debugAdapterType];
|
||||||
if (!c) {
|
const user = this._debugAdapterConfigs.get()[debugAdapterType];
|
||||||
|
const defaultNodeTableGraph = !Config.nodeTableGraphExcludedAdapters.has(
|
||||||
|
debugAdapterType
|
||||||
|
);
|
||||||
|
if (!builtIn && !user && !defaultNodeTableGraph) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const c = { nodeTableGraph: defaultNodeTableGraph, ...builtIn, ...user };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
context: c.context || "watch",
|
context: c.context || "watch",
|
||||||
|
nodeTableGraph: c.nodeTableGraph,
|
||||||
getFinalExpression: ({ expression, preferredExtractorId }) =>
|
getFinalExpression: ({ expression, preferredExtractorId }) =>
|
||||||
evaluateTemplate(c.expressionTemplate || "${expr}", {
|
evaluateTemplate(c.expressionTemplate || "${expr}", {
|
||||||
expr: expression,
|
expr: expression,
|
||||||
preferredDataExtractorId: preferredExtractorId || "",
|
preferredDataExtractorId: preferredExtractorId || "",
|
||||||
}),
|
}),
|
||||||
|
getSetupExpression: () => c.setupExpression,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,15 +154,19 @@ type DebugAdapterConfigs = {
|
|||||||
[debugAdapter: string]: {
|
[debugAdapter: string]: {
|
||||||
context?: "watch" | "repl";
|
context?: "watch" | "repl";
|
||||||
expressionTemplate?: "string";
|
expressionTemplate?: "string";
|
||||||
|
nodeTableGraph?: boolean;
|
||||||
|
setupExpression?: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DebugAdapterConfig {
|
export interface DebugAdapterConfig {
|
||||||
context: "watch" | "repl";
|
context: "watch" | "repl";
|
||||||
|
nodeTableGraph: boolean;
|
||||||
getFinalExpression(vars: {
|
getFinalExpression(vars: {
|
||||||
expression: string;
|
expression: string;
|
||||||
preferredExtractorId: DataExtractorId | undefined;
|
preferredExtractorId: DataExtractorId | undefined;
|
||||||
}): string;
|
}): string;
|
||||||
|
getSetupExpression(): string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluateTemplate(
|
function evaluateTemplate(
|
||||||
|
|||||||
@@ -131,4 +131,14 @@ export class DispatchingVisualizationBackend implements VisualizationBackend {
|
|||||||
)) || []
|
)) || []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async evaluateForEffect(
|
||||||
|
expression: string
|
||||||
|
): Promise<{ result: string } | { error: string }> {
|
||||||
|
const backend = this.activeVisualizationBackend;
|
||||||
|
if (!backend) {
|
||||||
|
return { error: "No active debug session." };
|
||||||
|
}
|
||||||
|
return await backend.evaluateForEffect(expression);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ import {
|
|||||||
GraphNode,
|
GraphNode,
|
||||||
GraphVisualizationData,
|
GraphVisualizationData,
|
||||||
} from "@hediet/debug-visualizer-data-extraction";
|
} from "@hediet/debug-visualizer-data-extraction";
|
||||||
|
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||||
|
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||||
|
import { FormattedMessage } from "../webviewContract";
|
||||||
import {
|
import {
|
||||||
DebugSessionVisualizationSupport,
|
DebugSessionVisualizationSupport,
|
||||||
VisualizationBackend,
|
|
||||||
GetVisualizationDataArgs,
|
GetVisualizationDataArgs,
|
||||||
|
VisualizationBackend,
|
||||||
VisualizationBackendBase,
|
VisualizationBackendBase,
|
||||||
} from "./VisualizationBackend";
|
} from "./VisualizationBackend";
|
||||||
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
|
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
|
||||||
import { FormattedMessage } from "../webviewContract";
|
|
||||||
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
|
||||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
|
||||||
|
|
||||||
registerUpdateReconciler(module);
|
registerUpdateReconciler(module);
|
||||||
|
|
||||||
@hotClass(module)
|
@hotClass(module)
|
||||||
export class GenericVisualizationSupport
|
export class GenericVisualizationSupport
|
||||||
implements DebugSessionVisualizationSupport {
|
implements DebugSessionVisualizationSupport {
|
||||||
constructor(private readonly debuggerView: DebuggerViewProxy) {}
|
constructor(private readonly debuggerView: DebuggerViewProxy) { }
|
||||||
|
|
||||||
createBackend(
|
createBackend(
|
||||||
session: DebugSessionProxy
|
session: DebugSessionProxy
|
||||||
@@ -63,6 +63,18 @@ export class GenericVisualizationBackend extends VisualizationBackendBase {
|
|||||||
context: this.getContext(),
|
context: this.getContext(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const result = parseEvaluationResultFromGenericDebugAdapter(
|
||||||
|
reply.result,
|
||||||
|
{
|
||||||
|
debugAdapterType:
|
||||||
|
this.debugSession.session.configuration.type,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.kind === "data") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Use structural information about variables
|
// Use structural information about variables
|
||||||
// from the evaluation response if present.
|
// from the evaluation response if present.
|
||||||
if (reply.variablesReference) {
|
if (reply.variablesReference) {
|
||||||
@@ -83,16 +95,10 @@ export class GenericVisualizationBackend extends VisualizationBackendBase {
|
|||||||
data: graph,
|
data: graph,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return parseEvaluationResultFromGenericDebugAdapter(
|
|
||||||
reply.result,
|
|
||||||
{
|
|
||||||
debugAdapterType: this.debugSession.session
|
|
||||||
.configuration.type,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
message: {
|
message: {
|
||||||
@@ -146,13 +152,13 @@ export class GenericVisualizationBackend extends VisualizationBackendBase {
|
|||||||
variablesReference: number;
|
variablesReference: number;
|
||||||
depth: number;
|
depth: number;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
source: undefined,
|
source: undefined,
|
||||||
label: rootLabel,
|
label: rootLabel,
|
||||||
variablesReference: rootVariablesReference,
|
variablesReference: rootVariablesReference,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let knownCount: number = 0;
|
let knownCount: number = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
getExpressionForDataExtractorApi,
|
ApiHasNotBeenInitializedCode,
|
||||||
DataResult,
|
CallFrameRequest,
|
||||||
getExpressionToInitializeDataExtractorApi,
|
CallFramesRequest,
|
||||||
DataExtractionResult,
|
DataExtractionResult,
|
||||||
getExpressionToDetectDataExtractorApiPresence,
|
DataExtractorId,
|
||||||
|
DataResult,
|
||||||
|
getExpressionForDataExtractorApi,
|
||||||
|
getExpressionToInitializeDataExtractorApi,
|
||||||
} from "@hediet/debug-visualizer-data-extraction";
|
} from "@hediet/debug-visualizer-data-extraction";
|
||||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||||
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
|
import { window } from "vscode";
|
||||||
|
import { Config } from "../Config";
|
||||||
|
import { DebugSessionProxy, StackFrame, StackTraceInfo } from "../proxies/DebugSessionProxy";
|
||||||
|
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||||
|
import { FileWatcher } from "../webview/WebviewConnection";
|
||||||
|
import { FormattedMessage } from "../webviewContract";
|
||||||
import {
|
import {
|
||||||
DebugSessionVisualizationSupport,
|
DebugSessionVisualizationSupport,
|
||||||
VisualizationBackend,
|
|
||||||
GetVisualizationDataArgs,
|
GetVisualizationDataArgs,
|
||||||
|
VisualizationBackend,
|
||||||
VisualizationBackendBase,
|
VisualizationBackendBase,
|
||||||
} from "./VisualizationBackend";
|
} from "./VisualizationBackend";
|
||||||
import { FormattedMessage } from "../webviewContract";
|
|
||||||
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
|
|
||||||
import { Config } from "../Config";
|
|
||||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
|
||||||
import { readFileSync, existsSync } from "fs";
|
|
||||||
import { reaction, observable } from "mobx";
|
|
||||||
import {
|
|
||||||
workspace,
|
|
||||||
FileSystemWatcher,
|
|
||||||
CancellationTokenSource,
|
|
||||||
CancellationToken,
|
|
||||||
window,
|
|
||||||
} from "vscode";
|
|
||||||
import { ResettableTimeout } from "@hediet/std/timer";
|
|
||||||
|
|
||||||
registerUpdateReconciler(module);
|
registerUpdateReconciler(module);
|
||||||
|
|
||||||
@hotClass(module)
|
@hotClass(module)
|
||||||
export class JsEvaluationEngine implements DebugSessionVisualizationSupport {
|
export class JsEvaluationEngine implements DebugSessionVisualizationSupport {
|
||||||
constructor(
|
constructor(private readonly debuggerView: DebuggerViewProxy, private readonly config: Config) {}
|
||||||
private readonly debuggerView: DebuggerViewProxy,
|
|
||||||
private readonly config: Config
|
|
||||||
) {}
|
|
||||||
|
|
||||||
createBackend(
|
createBackend(session: DebugSessionProxy): VisualizationBackend | undefined {
|
||||||
session: DebugSessionProxy
|
|
||||||
): VisualizationBackend | undefined {
|
|
||||||
const supportedDebugAdapters = [
|
const supportedDebugAdapters = [
|
||||||
"node",
|
"node",
|
||||||
"node2",
|
"node2",
|
||||||
@@ -52,26 +43,21 @@ export class JsEvaluationEngine implements DebugSessionVisualizationSupport {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
||||||
return new JsVisualizationBackend(
|
return new JsVisualizationBackend(session, this.debuggerView, this.config);
|
||||||
session,
|
|
||||||
this.debuggerView,
|
|
||||||
this.config
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CallFrameState {
|
||||||
|
public request: CallFramesRequest | undefined = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
class JsVisualizationBackend extends VisualizationBackendBase {
|
class JsVisualizationBackend extends VisualizationBackendBase {
|
||||||
public readonly expressionLanguageId = "javascript";
|
public readonly expressionLanguageId = "javascript";
|
||||||
private apiInitialized = false;
|
private initializePromise: Promise<void> | undefined = undefined;
|
||||||
private initializedPromise: Promise<void> | undefined = undefined;
|
|
||||||
|
|
||||||
constructor(
|
constructor(debugSession: DebugSessionProxy, debuggerView: DebuggerViewProxy, private readonly config: Config) {
|
||||||
debugSession: DebugSessionProxy,
|
|
||||||
debuggerView: DebuggerViewProxy,
|
|
||||||
private readonly config: Config
|
|
||||||
) {
|
|
||||||
super(debugSession, debuggerView);
|
super(debugSession, debuggerView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,95 +68,93 @@ class JsVisualizationBackend extends VisualizationBackendBase {
|
|||||||
return "repl";
|
return "repl";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getVisualizationData({
|
public async getVisualizationData(
|
||||||
expression,
|
args: GetVisualizationDataArgs
|
||||||
preferredExtractorId,
|
): Promise<{ kind: "data"; result: DataExtractionResult } | { kind: "error"; message: FormattedMessage }> {
|
||||||
}: GetVisualizationDataArgs): Promise<
|
let result = await this.getVisualizationDataIfInitialized(args);
|
||||||
|
|
||||||
|
if (result.kind === "not-initialized") {
|
||||||
|
await this.initializeApiOrWait();
|
||||||
|
result = await this.getVisualizationDataIfInitialized(args);
|
||||||
|
if (result.kind === "not-initialized") {
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
message: "Could not initialize API",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getVisualizationDataIfInitialized(
|
||||||
|
args: GetVisualizationDataArgs
|
||||||
|
): Promise<
|
||||||
| { kind: "data"; result: DataExtractionResult }
|
| { kind: "data"; result: DataExtractionResult }
|
||||||
| { kind: "error"; message: FormattedMessage }
|
| { kind: "error"; message: FormattedMessage }
|
||||||
|
| { kind: "not-initialized" }
|
||||||
> {
|
> {
|
||||||
|
const { expression, preferredExtractorId } = args;
|
||||||
|
let callFrameState: CallFrameState;
|
||||||
|
if (args.sessionStore.data instanceof CallFrameState) {
|
||||||
|
callFrameState = args.sessionStore.data;
|
||||||
|
} else {
|
||||||
|
callFrameState = new CallFrameState();
|
||||||
|
args.sessionStore.data = callFrameState;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ensureRuntimeIsInjected();
|
const frameId = this.debuggerView.getActiveStackFrameId(this.debugSession);
|
||||||
|
|
||||||
const frameId = this.debuggerView.getActiveStackFrameId(
|
const stackTrace = await this.debuggerView.activeDebugSession!.getStackTrace({
|
||||||
this.debugSession
|
threadId: 0,
|
||||||
);
|
levels: 20,
|
||||||
|
});
|
||||||
|
|
||||||
const variableNames = new Array<string>();
|
const variableNames = await getVariableNames(this.debugSession, frameId);
|
||||||
if (frameId) {
|
|
||||||
const scopes = await this.debugSession.getScopes({ frameId });
|
let tryCount = 0;
|
||||||
const scopeVariables = await Promise.all(
|
while (true) {
|
||||||
scopes
|
tryCount++;
|
||||||
.filter(s => !s.expensive)
|
|
||||||
.map(s =>
|
let callFrameDataValue = "null";
|
||||||
this.debugSession.getVariables({
|
if (callFrameState.request) {
|
||||||
variablesReference: s.variablesReference,
|
callFrameDataValue = await getCallFramesSnapshotValue(
|
||||||
})
|
this.debugSession,
|
||||||
)
|
stackTrace,
|
||||||
);
|
callFrameState.request
|
||||||
for (const variables of scopeVariables) {
|
|
||||||
variableNames.push(
|
|
||||||
...variables
|
|
||||||
.filter(v => v.value !== "undefined")
|
|
||||||
.map(v => v.name)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const result = await this.evaluateVisualizationDataRequest(
|
||||||
|
expression,
|
||||||
|
preferredExtractorId,
|
||||||
|
variableNames,
|
||||||
|
callFrameDataValue,
|
||||||
|
frameId
|
||||||
|
);
|
||||||
|
|
||||||
if (variableNames.length > 100) {
|
if (result.kind === "NoExtractors") {
|
||||||
variableNames.length = 100;
|
throw new Error("No extractors");
|
||||||
|
} else if (result.kind === "Error") {
|
||||||
|
throw new Error(result.message + "\n" + (result as any).stack);
|
||||||
|
} else if (result.kind === "Data") {
|
||||||
|
return {
|
||||||
|
kind: "data",
|
||||||
|
result: result.extractionResult,
|
||||||
|
};
|
||||||
|
} else if (result.kind === "OutdatedCallFrameSnapshot") {
|
||||||
|
if (tryCount >= 2) {
|
||||||
|
throw new Error("OutdatedCallFrameSnapshot");
|
||||||
|
}
|
||||||
|
callFrameState.request = result.callFramesRequest;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
const preferredExtractorExpr = preferredExtractorId
|
if (typeof error.message === "string" && error.message.indexOf(ApiHasNotBeenInitializedCode) !== -1) {
|
||||||
? `"${preferredExtractorId}"`
|
return { kind: "not-initialized" };
|
||||||
: "undefined";
|
|
||||||
|
|
||||||
const body = `${getExpressionForDataExtractorApi()}.getData(
|
|
||||||
e => (${expression}),
|
|
||||||
expr => eval(expr),
|
|
||||||
${preferredExtractorExpr},
|
|
||||||
{${variableNames.map(n => `${n}: ${n}`).join(",")}},
|
|
||||||
)`;
|
|
||||||
|
|
||||||
const wrappedExpr = `
|
|
||||||
(() => {
|
|
||||||
try {
|
|
||||||
return ${body};
|
|
||||||
} catch (e) {
|
|
||||||
return JSON.stringify({
|
|
||||||
kind: "Error",
|
|
||||||
message: e.message,
|
|
||||||
stack: e.stack
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
`;
|
|
||||||
|
|
||||||
const reply = await this.debugSession.evaluate({
|
|
||||||
expression: wrappedExpr,
|
|
||||||
frameId,
|
|
||||||
context: this.getContext(),
|
|
||||||
});
|
|
||||||
const resultStr = reply.result;
|
|
||||||
const jsonData =
|
|
||||||
this.getContext() === "copy"
|
|
||||||
? resultStr
|
|
||||||
: resultStr.substr(1, resultStr.length - 2);
|
|
||||||
const result = JSON.parse(jsonData) as DataResult;
|
|
||||||
|
|
||||||
if (result.kind === "NoExtractors") {
|
|
||||||
throw new Error("No extractors");
|
|
||||||
} else if (result.kind === "Error") {
|
|
||||||
throw new Error(result.message);
|
|
||||||
} else if (result.kind === "Data") {
|
|
||||||
return {
|
|
||||||
kind: "data",
|
|
||||||
result: result.extractionResult,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid Data");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
return {
|
return {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
message: error.message,
|
message: error.message,
|
||||||
@@ -178,218 +162,250 @@ class JsVisualizationBackend extends VisualizationBackendBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureRuntimeIsInjected(): Promise<void> | undefined {
|
private async evaluateVisualizationDataRequest(
|
||||||
if (this.apiInitialized) {
|
expression: string,
|
||||||
return;
|
preferredExtractorId: DataExtractorId | undefined,
|
||||||
}
|
variableNames: string[],
|
||||||
|
callFrameDataValue: string,
|
||||||
|
frameId: number | undefined
|
||||||
|
): Promise<DataResult> {
|
||||||
|
const preferredExtractorExpr = preferredExtractorId ? `"${preferredExtractorId}"` : "undefined";
|
||||||
|
const body = `${getExpressionForDataExtractorApi()}.getData(
|
||||||
|
() => (${expression}),
|
||||||
|
expr => eval(expr),
|
||||||
|
${preferredExtractorExpr},
|
||||||
|
{${variableNames.map((n) => `${n}: () => ${n}`).join(",")}},
|
||||||
|
${callFrameDataValue}
|
||||||
|
)`;
|
||||||
|
|
||||||
if (this.initializedPromise) {
|
const wrappedExpr = `(() => {
|
||||||
return this.initializedPromise;
|
try { return ${body}; }
|
||||||
} else {
|
catch (e) { return JSON.stringify({ kind: "Error", message: e.message, stack: e.stack }); }
|
||||||
this.initializedPromise = this._ensureRuntimeIsInjected();
|
})()
|
||||||
}
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
private async _ensureRuntimeIsInjected(): Promise<void> {
|
|
||||||
const body = getExpressionToDetectDataExtractorApiPresence();
|
|
||||||
|
|
||||||
const reply = await this.debugSession.evaluate({
|
const reply = await this.debugSession.evaluate({
|
||||||
expression: body,
|
expression: wrappedExpr,
|
||||||
frameId: undefined,
|
frameId,
|
||||||
context: this.getContext(),
|
context: this.getContext(),
|
||||||
});
|
});
|
||||||
const isApiInitialized = reply.result === "true";
|
const resultStr = reply.result;
|
||||||
|
const jsonData = this.getContext() === "copy" ? resultStr : resultStr.substr(1, resultStr.length - 2);
|
||||||
|
const result = JSON.parse(jsonData) as DataResult;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isApiInitialized) {
|
private initializeApiOrWait(): Promise<void> {
|
||||||
await this.initializeApi();
|
if (!this.initializePromise) {
|
||||||
|
this.initializePromise = this.initializeApi().finally(() => (this.initializePromise = undefined));
|
||||||
}
|
}
|
||||||
|
return this.initializePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeApi(): Promise<void> {
|
||||||
|
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
|
||||||
|
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
|
||||||
|
const frameId = this.debuggerView.getActiveStackFrameId(this.debugSession);
|
||||||
|
const result = await this.debugSession.evaluate({
|
||||||
|
expression,
|
||||||
|
frameId,
|
||||||
|
context: this.getContext(),
|
||||||
|
});
|
||||||
|
|
||||||
await this.initializeCustomScripts();
|
await this.initializeCustomScripts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeApi(): Promise<boolean> {
|
private customScripts: undefined | CustomScripts;
|
||||||
try {
|
|
||||||
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
|
|
||||||
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
|
|
||||||
|
|
||||||
await this.debugSession.evaluate({
|
|
||||||
expression,
|
|
||||||
frameId: undefined,
|
|
||||||
context: this.getContext(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable
|
|
||||||
private filesVersionId: number = 0;
|
|
||||||
|
|
||||||
private readonly fileChangeDebouncedScheduler = new DebouncedScheduler(100);
|
|
||||||
|
|
||||||
private async initializeCustomScripts(): Promise<void> {
|
private async initializeCustomScripts(): Promise<void> {
|
||||||
await this.debugSession.evaluate({
|
this.dispose.untrack(this.customScripts);
|
||||||
expression: `global["${injectedScriptsKey}"] = {};`,
|
this.customScripts?.dispose();
|
||||||
frameId: undefined,
|
this.customScripts = this.dispose.track(
|
||||||
context: "repl",
|
new CustomScripts(this.debugSession, this.debuggerView, this.config, () => this.onChangeEmitter.emit())
|
||||||
});
|
|
||||||
|
|
||||||
const watchers = new Map<string, WatchedFile>();
|
|
||||||
this.dispose.track({
|
|
||||||
dispose: () => {
|
|
||||||
for (const w of watchers.values()) {
|
|
||||||
w.watcher.dispose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
reaction(
|
|
||||||
() => ({
|
|
||||||
customScriptPaths: [...this.config.customScriptPaths],
|
|
||||||
version: this.filesVersionId,
|
|
||||||
}),
|
|
||||||
async ({ customScriptPaths }) => {
|
|
||||||
const promises = new Array<Promise<[Result, WatchedFile]>>();
|
|
||||||
|
|
||||||
const activePaths = new Set<string>();
|
|
||||||
for (const fsPath of customScriptPaths) {
|
|
||||||
activePaths.add(fsPath);
|
|
||||||
let entry = watchers.get(fsPath);
|
|
||||||
if (!entry) {
|
|
||||||
entry = new WatchedFile(fsPath, this.debugSession);
|
|
||||||
watchers.set(fsPath, entry);
|
|
||||||
entry.watcher.onDidChange(() => {
|
|
||||||
this.fileChangeDebouncedScheduler.run(() => {
|
|
||||||
this.filesVersionId++;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(entry.update().then(e => [e, entry!]));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [fsPath, entry] of watchers) {
|
|
||||||
if (!activePaths.has(fsPath)) {
|
|
||||||
promises.push(entry.remove().then(e => [e, entry]));
|
|
||||||
entry.watcher.dispose();
|
|
||||||
watchers.delete(fsPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
const errors = results.filter(([r]) => r.error);
|
|
||||||
for (const [e, watcher] of errors) {
|
|
||||||
window.showErrorMessage(
|
|
||||||
`Error while evaluating "${watcher.fsPath}": ${
|
|
||||||
e.error!.message
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promises.length > 0) {
|
|
||||||
this.onChangeEmitter.emit();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fireImmediately: true,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectedScriptsKey = `@hediet/debug-visualizer/injectedScripts`;
|
let globalCallFrameId = 0;
|
||||||
|
|
||||||
interface Result {
|
async function getCallFramesSnapshotValue(
|
||||||
error?: { message: string };
|
debugSession: DebugSessionProxy,
|
||||||
|
stackTrace: StackTraceInfo,
|
||||||
|
callFramesRequest: CallFramesRequest
|
||||||
|
): Promise<string> {
|
||||||
|
const globalFnDef = `
|
||||||
|
function getGlobal() {
|
||||||
|
if (typeof globalThis === "object") {
|
||||||
|
return globalThis;
|
||||||
|
} else if (typeof global === "object") {
|
||||||
|
return global;
|
||||||
|
} else if (typeof window === "object") {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
throw new Error("No global available");
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const callFrameId = globalCallFrameId++;
|
||||||
|
let idx = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
const skippedCounts: Record<number, number> = {};
|
||||||
|
|
||||||
|
let frames = stackTrace.stackFrames;
|
||||||
|
const firstAsyncFrameIdx = frames.findIndex((f) => f.id === 0);
|
||||||
|
if (firstAsyncFrameIdx !== -1) {
|
||||||
|
frames = frames.slice(0, firstAsyncFrameIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Promise.all(
|
||||||
|
frames.map(async (frame) => {
|
||||||
|
function matches(frame: StackFrame, callFrameRequest: CallFrameRequest): boolean {
|
||||||
|
if (frame.name !== callFrameRequest.methodName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callFramesRequest.requestedCallFrames.some((r) => matches(frame, r))) {
|
||||||
|
if (skippedCount === 0) {
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
skippedCount++;
|
||||||
|
skippedCounts[idx - 1] = skippedCount;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
skippedCount = 0;
|
||||||
|
const curFrameIdx = idx;
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
const scopes = await debugSession.getScopes({ frameId: frame.id });
|
||||||
|
const scopeVariables = await Promise.all(
|
||||||
|
scopes
|
||||||
|
.filter((s) => !s.expensive && s.name.indexOf(":") !== -1)
|
||||||
|
.map((s) => debugSession.getVariables({ variablesReference: s.variablesReference }))
|
||||||
|
);
|
||||||
|
const variables = new Set(scopeVariables.flatMap((v) => v.map((v) => v.name)));
|
||||||
|
|
||||||
|
await debugSession.evaluate({
|
||||||
|
expression: `
|
||||||
|
(() => {
|
||||||
|
${globalFnDef}
|
||||||
|
const g = getGlobal();
|
||||||
|
(g.$$hedietDbgVslzr${callFrameId} = g.$$hedietDbgVslzr${callFrameId} || [])[${curFrameIdx}] = {
|
||||||
|
vars: { ${[...variables].map((v) => `${v}: ${v}`).join(", ")} },
|
||||||
|
...${JSON.stringify({
|
||||||
|
methodName: frame.name,
|
||||||
|
source: { path: frame.source.path, name: frame.source.name },
|
||||||
|
})}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
`,
|
||||||
|
frameId: frame.id,
|
||||||
|
context: "repl",
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.some((r) => r)) {
|
||||||
|
return `{ requestId: ${JSON.stringify(callFramesRequest.requestId)}, frames: [] }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
(() => {
|
||||||
|
${globalFnDef}
|
||||||
|
const g = getGlobal();
|
||||||
|
const r = g.$$hedietDbgVslzr${callFrameId};
|
||||||
|
delete g.$$hedietDbgVslzr${callFrameId};
|
||||||
|
${Object.entries(skippedCounts)
|
||||||
|
.map(([idx, val]) => `r[${idx}] = { skippedFrames: ${val} };\n`)
|
||||||
|
.join("")}
|
||||||
|
return { requestId: ${JSON.stringify(callFramesRequest.requestId)}, frames: r };
|
||||||
|
})()
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WatchedFile {
|
async function getVariableNames(debugSession: DebugSessionProxy, frameId: number | undefined) {
|
||||||
public static nextId = 0;
|
const variableNames = new Array<string>();
|
||||||
public readonly watcher = workspace.createFileSystemWatcher(this.fsPath);
|
if (frameId) {
|
||||||
public readonly id = WatchedFile.nextId++;
|
const scopes = await debugSession.getScopes({ frameId });
|
||||||
public lastContent: string | undefined = undefined;
|
const scopeVariables = await Promise.all(
|
||||||
|
scopes
|
||||||
|
.filter((s) => !s.expensive && s.name !== "Global")
|
||||||
|
.map((s) =>
|
||||||
|
debugSession.getVariables({
|
||||||
|
variablesReference: s.variablesReference,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const variables of scopeVariables) {
|
||||||
|
variableNames.push(...variables.filter((v) => v.value !== "undefined").map((v) => v.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variableNames.length > 50) {
|
||||||
|
variableNames.length = 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return variableNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomScripts {
|
||||||
|
public readonly dispose = Disposable.fn();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly fsPath: string,
|
debugSession: DebugSessionProxy,
|
||||||
private readonly debugSession: DebugSessionProxy
|
debuggerView: DebuggerViewProxy,
|
||||||
) {}
|
config: Config,
|
||||||
|
changeHandler: () => void
|
||||||
|
) {
|
||||||
|
this.dispose.track(
|
||||||
|
new FileWatcher(
|
||||||
|
() => config.customScriptPaths,
|
||||||
|
async (files) => {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.fileExists) {
|
||||||
|
window.showErrorMessage(`The file ${file.path} does not exist.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
public async update(): Promise<Result> {
|
let expression = `
|
||||||
if (!existsSync(this.fsPath)) {
|
(
|
||||||
return {
|
runCode => {
|
||||||
error: { message: `File does not exist.` },
|
let fn = undefined;
|
||||||
};
|
if (runCode) {
|
||||||
}
|
const module = {};
|
||||||
|
runCode(module);
|
||||||
|
fn = module.exports;
|
||||||
|
}
|
||||||
|
${getExpressionForDataExtractorApi()}.setDataExtractorFn(
|
||||||
|
${JSON.stringify(file.path)},
|
||||||
|
fn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(
|
||||||
|
${file.content === undefined ? "undefined" : `function (module) { ${file.content} }`}
|
||||||
|
)`;
|
||||||
|
|
||||||
const scriptContent = readFileSync(this.fsPath, {
|
try {
|
||||||
encoding: "utf-8",
|
await debugSession.evaluate({
|
||||||
});
|
expression,
|
||||||
|
frameId: debuggerView.getActiveStackFrameId(debugSession),
|
||||||
|
context: "repl",
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
window.showErrorMessage(
|
||||||
|
'Error while running custom visualization extractor script "' +
|
||||||
|
file.path +
|
||||||
|
'": ' +
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastContent === scriptContent) {
|
changeHandler();
|
||||||
return {};
|
|
||||||
}
|
|
||||||
this.lastContent = scriptContent;
|
|
||||||
|
|
||||||
const expression = `
|
|
||||||
(
|
|
||||||
load => {
|
|
||||||
var module = {};
|
|
||||||
load(module);
|
|
||||||
global["${injectedScriptsKey}"]["${this.id}"] = module.exports;
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(
|
);
|
||||||
function (module) { ${scriptContent} }
|
|
||||||
)`;
|
|
||||||
try {
|
|
||||||
await this.debugSession.evaluate({
|
|
||||||
expression,
|
|
||||||
frameId: undefined,
|
|
||||||
context: "repl",
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return { error: { message: e.message } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(): Promise<Result> {
|
|
||||||
const expression = `(() => { delete global["${injectedScriptsKey}"]["${this.id}"]; })()`;
|
|
||||||
try {
|
|
||||||
await this.debugSession.evaluate({
|
|
||||||
expression,
|
|
||||||
frameId: undefined,
|
|
||||||
context: "repl",
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return { error: { message: e.message } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DebouncedScheduler {
|
|
||||||
private timeout: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
constructor(private readonly debounceTimeout: number) {}
|
|
||||||
|
|
||||||
public run(action: () => void): void {
|
|
||||||
this.clear();
|
|
||||||
this.timeout = setTimeout(action, this.debounceTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clear() {
|
|
||||||
if (this.timeout) {
|
|
||||||
clearTimeout(this.timeout);
|
|
||||||
this.timeout = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
this.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,676 @@
|
|||||||
|
import {
|
||||||
|
DataExtractionResult,
|
||||||
|
VisualizationData,
|
||||||
|
} from "@hediet/debug-visualizer-data-extraction";
|
||||||
|
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||||
|
import { Config, DebugAdapterConfig } from "../Config";
|
||||||
|
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||||
|
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||||
|
import { FormattedMessage } from "../webviewContract";
|
||||||
|
import {
|
||||||
|
DebugSessionVisualizationSupport,
|
||||||
|
GetVisualizationDataArgs,
|
||||||
|
VisualizationBackend,
|
||||||
|
VisualizationBackendBase,
|
||||||
|
} from "./VisualizationBackend";
|
||||||
|
|
||||||
|
registerUpdateReconciler(module);
|
||||||
|
|
||||||
|
// A generic, DAP-only replacement for the old gdb_debug_graph.py/
|
||||||
|
// py_debug_graph.py debuggee-side scripts: walks whatever the debug adapter
|
||||||
|
// already exposes via the standard "evaluate"/"variables"/"scopes" requests
|
||||||
|
// and produces the same { kind: { nodeTable: true }, nodes, edges, roots }
|
||||||
|
// shape the node-table-graph webview visualizer expects (see
|
||||||
|
// webview/src/visualizers/nodeTableGraphVisualizer.ts) -- no script needs to
|
||||||
|
// be sourced into gdb or imported into the debuggee's Python process.
|
||||||
|
|
||||||
|
interface NodeField {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
isPointer?: boolean;
|
||||||
|
typeHint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArrayTableColumn {
|
||||||
|
id: string;
|
||||||
|
// Aligned by index with the owning ArrayTable's memberNames -- a member
|
||||||
|
// this particular element doesn't have gets an empty-value placeholder
|
||||||
|
// rather than shifting the columns out of alignment.
|
||||||
|
values: NodeField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArrayTable {
|
||||||
|
memberNames: string[];
|
||||||
|
columns: ArrayTableColumn[];
|
||||||
|
// The real element count and whether `columns` was capped at
|
||||||
|
// MAX_ARRAY_ELEMENTS -- lets the webview show "N of M" instead of
|
||||||
|
// silently hiding that the rest of a very large array wasn't fetched.
|
||||||
|
totalCount: number;
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphNodeData {
|
||||||
|
id: string;
|
||||||
|
fields: NodeField[];
|
||||||
|
type?: string;
|
||||||
|
// Present instead of a meaningful `fields` (left []) when this node is an
|
||||||
|
// array/slice/vector of struct-shaped elements -- rendered as a compact
|
||||||
|
// table (one row per member, one column per element) instead of a chain
|
||||||
|
// of per-element boxes. See looksLikeArrayOfStructs/buildArrayTableNode.
|
||||||
|
arrayTable?: ArrayTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphEdge {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphRoot {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
kind?: "local" | "watched" | "global";
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeTableVisualizationData extends VisualizationData {
|
||||||
|
kind: { nodeTable: true };
|
||||||
|
lang: string;
|
||||||
|
nodes: GraphNodeData[];
|
||||||
|
edges: GraphEdge[];
|
||||||
|
roots: GraphRoot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DapVariable {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
variablesReference: number;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A field only becomes its own graph node (and edge) when it looks like a
|
||||||
|
// genuine pointer with an independent address -- everything else (by-value
|
||||||
|
// nested structs, Option/std::optional wrappers, ...) is flattened inline as
|
||||||
|
// dotted sub-fields instead, up to this depth. Once `-enable-pretty-printing`
|
||||||
|
// is on (see Config.ts's built-in cppdbg setupExpression), a wrapper's DAP
|
||||||
|
// children already reflect gdb's own pretty-printer output, so this stays a
|
||||||
|
// shallow structural rule rather than needing to special-case any container
|
||||||
|
// type by name.
|
||||||
|
const INLINE_FLATTEN_MAX_DEPTH = 3;
|
||||||
|
const ADDRESS_RE = /0x[0-9a-f]+/i;
|
||||||
|
|
||||||
|
// Fetching (and walking the members of) every element of a very large
|
||||||
|
// array/slice/vector is both slow (one DAP round trip per element) and
|
||||||
|
// produces an unusably wide table -- cap it and let the webview page
|
||||||
|
// through what's fetched instead of hanging or rendering thousands of
|
||||||
|
// columns at once.
|
||||||
|
const MAX_ARRAY_ELEMENTS = 500;
|
||||||
|
|
||||||
|
// pydevd/debugpy (Python) injects these as synthetic pseudo-children into
|
||||||
|
// *every* object's variable listing, grouping dunder attributes and bound
|
||||||
|
// methods for the Variables view -- they aren't real fields. Left
|
||||||
|
// unfiltered, the inline-flatten path in addField() (which has no notion of
|
||||||
|
// "this isn't a real field, don't recurse") walks straight into them: each
|
||||||
|
// one is itself an object with its own nested "special variables", so it
|
||||||
|
// recurses into Python's dict/type/method-wrapper internals. Measured on a
|
||||||
|
// 3-node linked list: ~126 DAP round trips and 400+ flattened junk fields,
|
||||||
|
// several seconds, on every single refresh -- the "Debug Visualizer
|
||||||
|
// freezes" symptom reported against a Python (debugpy) session. gdb/delve
|
||||||
|
// have no equivalent synthetic grouping, so this is debugpy-specific.
|
||||||
|
const PSEUDO_SCOPE_NAMES = new Set([
|
||||||
|
"special variables",
|
||||||
|
"function variables",
|
||||||
|
"class variables",
|
||||||
|
]);
|
||||||
|
|
||||||
|
@hotClass(module)
|
||||||
|
export class NodeTableGraphEngine implements DebugSessionVisualizationSupport {
|
||||||
|
constructor(
|
||||||
|
private readonly debuggerView: DebuggerViewProxy,
|
||||||
|
private readonly config: Config
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createBackend(
|
||||||
|
session: DebugSessionProxy
|
||||||
|
): VisualizationBackend | undefined {
|
||||||
|
const config = this.config.getDebugAdapterConfig(session.session.type);
|
||||||
|
if (!config || !config.nodeTableGraph) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return new NodeTableGraphVisualizationBackend(
|
||||||
|
session,
|
||||||
|
this.debuggerView,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeTableGraphVisualizationBackend extends VisualizationBackendBase {
|
||||||
|
public readonly expressionLanguageId = "text";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
debugSession: DebugSessionProxy,
|
||||||
|
debuggerView: DebuggerViewProxy,
|
||||||
|
private readonly config: DebugAdapterConfig
|
||||||
|
) {
|
||||||
|
super(debugSession, debuggerView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectLang(): string {
|
||||||
|
const type = this.debugSession.session.type;
|
||||||
|
if (type === "debugpy" || type === "python") {
|
||||||
|
return "python";
|
||||||
|
}
|
||||||
|
if (type === "go") {
|
||||||
|
return "go";
|
||||||
|
}
|
||||||
|
const miDebuggerPath = String(
|
||||||
|
(this.debugSession.session.configuration as { miDebuggerPath?: string })
|
||||||
|
.miDebuggerPath || ""
|
||||||
|
);
|
||||||
|
if (miDebuggerPath.includes("rust-gdb")) {
|
||||||
|
return "rust";
|
||||||
|
}
|
||||||
|
return "cpp";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVisualizationData({
|
||||||
|
expression,
|
||||||
|
sessionStore,
|
||||||
|
}: GetVisualizationDataArgs): Promise<
|
||||||
|
| { kind: "data"; result: DataExtractionResult }
|
||||||
|
| { kind: "error"; message: FormattedMessage }
|
||||||
|
> {
|
||||||
|
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||||
|
this.debugSession
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.runSetupOnce(sessionStore, frameId);
|
||||||
|
|
||||||
|
const nodes = new Map<string, GraphNodeData>();
|
||||||
|
const edges: GraphEdge[] = [];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const roots: GraphRoot[] = [];
|
||||||
|
|
||||||
|
const primary = await this.debugSession.evaluate({
|
||||||
|
expression,
|
||||||
|
frameId,
|
||||||
|
context: this.config.context,
|
||||||
|
});
|
||||||
|
|
||||||
|
let primaryId: string | undefined;
|
||||||
|
if (primary.variablesReference > 0) {
|
||||||
|
primaryId = await this.walkNodeOrArrayTable(
|
||||||
|
primary.variablesReference,
|
||||||
|
primary.result,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
visited,
|
||||||
|
undefined,
|
||||||
|
primary.type
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nodes.set("root", {
|
||||||
|
id: "root",
|
||||||
|
fields: [{ name: "value", value: primary.result }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.discoverLocals(frameId, primaryId, nodes, edges, visited, roots);
|
||||||
|
|
||||||
|
const data: NodeTableVisualizationData = {
|
||||||
|
kind: { nodeTable: true },
|
||||||
|
lang: this.detectLang(),
|
||||||
|
nodes: [...nodes.values()],
|
||||||
|
edges,
|
||||||
|
roots,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "data",
|
||||||
|
result: {
|
||||||
|
availableExtractors: [],
|
||||||
|
usedExtractor: {
|
||||||
|
id: "node-table-graph" as any,
|
||||||
|
name: "Node Table Graph",
|
||||||
|
priority: 2000,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
message: {
|
||||||
|
kind: "list",
|
||||||
|
items: [
|
||||||
|
"An error occurred while building the node table graph:",
|
||||||
|
error.message,
|
||||||
|
`Used debug adapter: ${this.debugSession.session.configuration.type}`,
|
||||||
|
{
|
||||||
|
kind: "inlineList",
|
||||||
|
items: [
|
||||||
|
"Evaluated expression is",
|
||||||
|
{ kind: "code", content: expression },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the adapter's setup commands (e.g. gdb's pretty-printing/
|
||||||
|
// print-elements/print-characters options) once per debug session
|
||||||
|
// instead of on every refresh -- sessionStore.data is a per-session slot
|
||||||
|
// VisualizationWatchModel already keeps around for exactly this purpose.
|
||||||
|
private async runSetupOnce(
|
||||||
|
sessionStore: { data: unknown },
|
||||||
|
frameId: number | undefined
|
||||||
|
) {
|
||||||
|
const store = sessionStore.data as
|
||||||
|
| { nodeTableGraphSetupDone?: boolean }
|
||||||
|
| undefined;
|
||||||
|
if (store && store.nodeTableGraphSetupDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const commands = this.config.getSetupExpression();
|
||||||
|
if (commands) {
|
||||||
|
for (const cmd of commands) {
|
||||||
|
try {
|
||||||
|
await this.debugSession.evaluate({
|
||||||
|
expression: cmd,
|
||||||
|
frameId,
|
||||||
|
context: "repl",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// best-effort -- fall through, the real walk below will
|
||||||
|
// still surface a clear error if something's actually wrong
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessionStore.data = { ...(store || {}), nodeTableGraphSetupDone: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async discoverLocals(
|
||||||
|
frameId: number | undefined,
|
||||||
|
primaryId: string | undefined,
|
||||||
|
nodes: Map<string, GraphNodeData>,
|
||||||
|
edges: GraphEdge[],
|
||||||
|
visited: Set<string>,
|
||||||
|
roots: GraphRoot[]
|
||||||
|
) {
|
||||||
|
if (frameId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scopes = await this.debugSession.getScopes({ frameId });
|
||||||
|
const localsScope = scopes.find((s) => /local/i.test(s.name));
|
||||||
|
if (!localsScope) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const localVars = await this.debugSession.getVariables({
|
||||||
|
variablesReference: localsScope.variablesReference,
|
||||||
|
});
|
||||||
|
for (const v of localVars as DapVariable[]) {
|
||||||
|
const addr = this.extractAddress(v.value);
|
||||||
|
if (primaryId !== undefined && addr === primaryId) {
|
||||||
|
continue; // already covered by the primary root
|
||||||
|
}
|
||||||
|
if (v.variablesReference > 0) {
|
||||||
|
const id = await this.walkNodeOrArrayTable(
|
||||||
|
v.variablesReference,
|
||||||
|
v.value,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
visited,
|
||||||
|
undefined,
|
||||||
|
v.type
|
||||||
|
);
|
||||||
|
roots.push({ name: v.name, value: id, kind: "local", type: v.type });
|
||||||
|
} else {
|
||||||
|
roots.push({
|
||||||
|
name: v.name,
|
||||||
|
value: v.value,
|
||||||
|
kind: "local",
|
||||||
|
type: v.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds an address that plausibly belongs to `value` *itself*, not to
|
||||||
|
// something nested inside its own summary text. Some adapters (delve,
|
||||||
|
// with dlvLoadConfig's followPointers on) inline a pointer's entire
|
||||||
|
// pointee summary -- including further nested pointers' addresses --
|
||||||
|
// into the value string without ever printing the pointer's own
|
||||||
|
// address anywhere; naively taking the first "0x..." match there would
|
||||||
|
// silently misattribute some nested field's address as this value's own
|
||||||
|
// identity. Rejecting a match that only appears after the value's own
|
||||||
|
// summary has already opened a nested struct/array (a "{" or "[")
|
||||||
|
// filters those out, while still matching the common bare-address
|
||||||
|
// ("0x1234") and gdb-style ("0x1234 <Node>") and Python-repr
|
||||||
|
// ("<Foo object at 0x1234>") shapes, none of which have that.
|
||||||
|
private extractAddress(value: string): string | undefined {
|
||||||
|
const m = ADDRESS_RE.exec(value);
|
||||||
|
if (!m) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (/[{[]/.test(value.slice(0, m.index))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return m[0].toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some debug adapters (delve/Go, at least) represent a pointer's
|
||||||
|
// dereference as a single transparent child with an empty name, rather
|
||||||
|
// than exposing the pointee's fields/elements directly -- unwrap through
|
||||||
|
// it (recursively, in case of multiple indirections, e.g. `**T`) so
|
||||||
|
// field names and array detection see the real members, not one
|
||||||
|
// anonymous wrapper hop away from them.
|
||||||
|
private async getResolvedChildren(
|
||||||
|
variablesReference: number
|
||||||
|
): Promise<DapVariable[]> {
|
||||||
|
let children = await this.fetchRealChildren(variablesReference);
|
||||||
|
while (
|
||||||
|
children.length === 1 &&
|
||||||
|
children[0].name === "" &&
|
||||||
|
children[0].variablesReference > 0
|
||||||
|
) {
|
||||||
|
children = await this.fetchRealChildren(children[0].variablesReference);
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters out pydevd/debugpy's synthetic "special variables"/"function
|
||||||
|
// variables" pseudo-children (see PSEUDO_SCOPE_NAMES) right at the
|
||||||
|
// source, so nothing downstream -- array-of-structs detection, the
|
||||||
|
// inline-flatten walk, array table columns -- ever has to know they
|
||||||
|
// exist.
|
||||||
|
private async fetchRealChildren(
|
||||||
|
variablesReference: number
|
||||||
|
): Promise<DapVariable[]> {
|
||||||
|
const children = (await this.debugSession.getVariables({
|
||||||
|
variablesReference,
|
||||||
|
})) as DapVariable[];
|
||||||
|
return children.filter((c) => !PSEUDO_SCOPE_NAMES.has(c.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private looksLikePointer(v: { type?: string; value: string }): boolean {
|
||||||
|
if (v.type) {
|
||||||
|
// C/C++ (rust-gdb prints Rust pointers/references the same way
|
||||||
|
// gdb's own C type-printer would): trailing sigil, e.g. "Node *".
|
||||||
|
if (/\*\s*$/.test(v.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Go (delve) and Rust's own `*mut T`/`*const T`/`&T` syntax put
|
||||||
|
// the sigil in front instead, e.g. "*main.Node", "&Node<i32>".
|
||||||
|
if (/^\s*[*&]/.test(v.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/<.*object at 0x[0-9a-f]+>/i.test(v.value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A value counts as "array of classes" when every child's name is a bare
|
||||||
|
// or bracketed index (0,1,2,... or [0],[1],[2],...) -- the DAP naming
|
||||||
|
// convention arrays/slices/vectors already use across gdb/MIEngine,
|
||||||
|
// debugpy, and delve -- and the first element is itself struct-shaped
|
||||||
|
// (has its own children), not a scalar leaf. Only the first element is
|
||||||
|
// checked, on the assumption the array is homogeneous (true for every
|
||||||
|
// language this targets); a scalar array (e.g. `int[5]`) intentionally
|
||||||
|
// falls through to the ordinary inline-flatten path instead.
|
||||||
|
private looksLikeArrayOfStructs(children: DapVariable[]): boolean {
|
||||||
|
if (children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!children.every((c) => /^\[?\d+\]?$/.test(c.name))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return children[0].variablesReference > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively walks a DAP variablesReference into `nodes`/`edges`,
|
||||||
|
// returning the id of the node it produced -- a real address when one
|
||||||
|
// is present in the variable's own printed value, else the DAP
|
||||||
|
// variablesReference itself (verified against a real debugpy session,
|
||||||
|
// including through a genuine self-referential cycle, to be stably
|
||||||
|
// reused for the same underlying object -- not just an arbitrary
|
||||||
|
// per-call counter, which would neither dedup re-visits of the same
|
||||||
|
// addressless object nor terminate a true cycle). Mirrors the old
|
||||||
|
// scripts' hex(ptr_int)/id(obj)-derived node ids, giving the same
|
||||||
|
// cross-reference cycle dedup via `visited`. Routes to either a
|
||||||
|
// regular struct node or an array-table node depending on shape, so an
|
||||||
|
// array of structs gets the compact table rendering everywhere it
|
||||||
|
// appears -- as the primary root, a struct's own field, a local, or a
|
||||||
|
// pointer's target -- not just in one specific position.
|
||||||
|
private async walkNodeOrArrayTable(
|
||||||
|
variablesReference: number,
|
||||||
|
ownValue: string,
|
||||||
|
nodes: Map<string, GraphNodeData>,
|
||||||
|
edges: GraphEdge[],
|
||||||
|
visited: Set<string>,
|
||||||
|
prefetchedChildren?: DapVariable[],
|
||||||
|
ownType?: string
|
||||||
|
): Promise<string> {
|
||||||
|
const id = this.extractAddress(ownValue) || `vref:${variablesReference}`;
|
||||||
|
if (visited.has(id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
visited.add(id);
|
||||||
|
|
||||||
|
const children =
|
||||||
|
prefetchedChildren ?? (await this.getResolvedChildren(variablesReference));
|
||||||
|
|
||||||
|
if (this.looksLikeArrayOfStructs(children)) {
|
||||||
|
await this.buildArrayTableNode(id, children, nodes, edges, visited);
|
||||||
|
} else {
|
||||||
|
await this.buildStructFields(id, children, nodes, edges, visited, ownType);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildStructFields(
|
||||||
|
id: string,
|
||||||
|
children: DapVariable[],
|
||||||
|
nodes: Map<string, GraphNodeData>,
|
||||||
|
edges: GraphEdge[],
|
||||||
|
visited: Set<string>,
|
||||||
|
ownType?: string
|
||||||
|
) {
|
||||||
|
const fields: NodeField[] = [];
|
||||||
|
for (const child of children) {
|
||||||
|
await this.addField(child, fields, edges, nodes, visited, id, 0, ownType);
|
||||||
|
}
|
||||||
|
nodes.set(id, { id, fields, type: ownType });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a single node holding every element's members as columns
|
||||||
|
// (memberNames is the union of member names across elements, in
|
||||||
|
// first-seen order, so a ragged/heterogeneous array still lines up
|
||||||
|
// sensibly). A pointer member is still a real edge out of this same
|
||||||
|
// node for each element that has one, labeled "[i].member" -- pointers
|
||||||
|
// stay navigable everywhere, they just fan out from one box instead of
|
||||||
|
// each element getting its own.
|
||||||
|
private async buildArrayTableNode(
|
||||||
|
id: string,
|
||||||
|
allElements: DapVariable[],
|
||||||
|
nodes: Map<string, GraphNodeData>,
|
||||||
|
edges: GraphEdge[],
|
||||||
|
visited: Set<string>
|
||||||
|
) {
|
||||||
|
const totalCount = allElements.length;
|
||||||
|
const truncated = totalCount > MAX_ARRAY_ELEMENTS;
|
||||||
|
const elements = truncated
|
||||||
|
? allElements.slice(0, MAX_ARRAY_ELEMENTS)
|
||||||
|
: allElements;
|
||||||
|
|
||||||
|
const memberNames: string[] = [];
|
||||||
|
const seenMembers = new Set<string>();
|
||||||
|
const columnCells: Map<string, NodeField>[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
const cells = new Map<string, NodeField>();
|
||||||
|
const el = elements[i];
|
||||||
|
if (el.variablesReference > 0) {
|
||||||
|
const members = await this.getResolvedChildren(el.variablesReference);
|
||||||
|
for (const m of members) {
|
||||||
|
if (!seenMembers.has(m.name)) {
|
||||||
|
seenMembers.add(m.name);
|
||||||
|
memberNames.push(m.name);
|
||||||
|
}
|
||||||
|
const addr = this.extractAddress(m.value);
|
||||||
|
if (addr && this.looksLikePointer(m)) {
|
||||||
|
if (m.variablesReference > 0) {
|
||||||
|
const targetId = await this.walkNodeOrArrayTable(
|
||||||
|
m.variablesReference,
|
||||||
|
m.value,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
visited,
|
||||||
|
undefined,
|
||||||
|
m.type
|
||||||
|
);
|
||||||
|
edges.push({
|
||||||
|
from: id,
|
||||||
|
to: targetId,
|
||||||
|
label: `[${i}].${m.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cells.set(m.name, {
|
||||||
|
name: m.name,
|
||||||
|
value: addr,
|
||||||
|
isPointer: true,
|
||||||
|
typeHint: m.type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cells.set(m.name, {
|
||||||
|
name: m.name,
|
||||||
|
value: m.value,
|
||||||
|
typeHint: m.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columnCells.push(cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: ArrayTableColumn[] = elements.map((el, i) => ({
|
||||||
|
id: /^\[\d+\]$/.test(el.name) ? el.name : `[${el.name}]`,
|
||||||
|
values: memberNames.map(
|
||||||
|
(m) => columnCells[i].get(m) || { name: m, value: "" }
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
nodes.set(id, {
|
||||||
|
id,
|
||||||
|
fields: [],
|
||||||
|
arrayTable: { memberNames, columns, totalCount, truncated },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addField(
|
||||||
|
child: DapVariable,
|
||||||
|
fields: NodeField[],
|
||||||
|
edges: GraphEdge[],
|
||||||
|
nodes: Map<string, GraphNodeData>,
|
||||||
|
visited: Set<string>,
|
||||||
|
ownerId: string,
|
||||||
|
depth: number,
|
||||||
|
ownerType?: string
|
||||||
|
) {
|
||||||
|
const addr = this.extractAddress(child.value);
|
||||||
|
// A field whose *static type* matches the type of the struct that
|
||||||
|
// directly contains it (not necessarily the original top-level
|
||||||
|
// owner -- see the recursive call below, which passes child.type
|
||||||
|
// as the new ownerType) can only be sound if it's some kind of
|
||||||
|
// reference/handle rather than a true by-value member: a value type
|
||||||
|
// genuinely containing itself would have infinite size. This is
|
||||||
|
// what catches Python's (and similarly Java/C#'s) recursive
|
||||||
|
// dataclasses/objects, which never print an address in their repr
|
||||||
|
// (looksLikePointer's <object at 0x...> check only fires for
|
||||||
|
// classes that haven't overridden __repr__ -- dataclasses always
|
||||||
|
// do) and so would otherwise fall through to the inline-flatten
|
||||||
|
// path below and get capped/mangled at INLINE_FLATTEN_MAX_DEPTH
|
||||||
|
// instead of forming a proper linked node graph.
|
||||||
|
const isSelfReferential = !!ownerType && !!child.type && child.type === ownerType;
|
||||||
|
if ((addr && this.looksLikePointer(child)) || isSelfReferential) {
|
||||||
|
let targetId: string | undefined;
|
||||||
|
if (child.variablesReference > 0) {
|
||||||
|
targetId = await this.walkNodeOrArrayTable(
|
||||||
|
child.variablesReference,
|
||||||
|
child.value,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
visited,
|
||||||
|
undefined,
|
||||||
|
child.type
|
||||||
|
);
|
||||||
|
edges.push({ from: ownerId, to: targetId, label: child.name });
|
||||||
|
}
|
||||||
|
fields.push({
|
||||||
|
name: child.name,
|
||||||
|
// Real pointers show their address; a self-referential
|
||||||
|
// field with no address of its own shows the id of the
|
||||||
|
// node it resolves to instead (still a stable, clickable
|
||||||
|
// cross-reference) -- child.value as a last-resort fallback
|
||||||
|
// covers a null/childless self-typed field (unusual, but
|
||||||
|
// keeps this from rendering an empty value).
|
||||||
|
value: addr || targetId || child.value,
|
||||||
|
isPointer: true,
|
||||||
|
typeHint: child.type,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.variablesReference > 0) {
|
||||||
|
const grandchildren = await this.getResolvedChildren(
|
||||||
|
child.variablesReference
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.looksLikeArrayOfStructs(grandchildren)) {
|
||||||
|
const targetId = await this.walkNodeOrArrayTable(
|
||||||
|
child.variablesReference,
|
||||||
|
child.value,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
visited,
|
||||||
|
grandchildren
|
||||||
|
);
|
||||||
|
edges.push({ from: ownerId, to: targetId, label: child.name });
|
||||||
|
fields.push({
|
||||||
|
name: child.name,
|
||||||
|
value: targetId,
|
||||||
|
isPointer: true,
|
||||||
|
typeHint: child.type,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth < INLINE_FLATTEN_MAX_DEPTH) {
|
||||||
|
if (grandchildren.length === 0) {
|
||||||
|
fields.push({ name: child.name, value: child.value, typeHint: child.type });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const gc of grandchildren) {
|
||||||
|
await this.addField(
|
||||||
|
{ ...gc, name: `${child.name}.${gc.name}` },
|
||||||
|
fields,
|
||||||
|
edges,
|
||||||
|
nodes,
|
||||||
|
visited,
|
||||||
|
ownerId,
|
||||||
|
depth + 1,
|
||||||
|
child.type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.push({ name: child.name, value: child.value, typeHint: child.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
145
extension/src/VisualizationBackend/PyVisualizationSupport.ts
Normal file
145
extension/src/VisualizationBackend/PyVisualizationSupport.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
DataExtractionResult,
|
||||||
|
DataExtractorId,
|
||||||
|
} from "@hediet/debug-visualizer-data-extraction";
|
||||||
|
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||||
|
import { Config } from "../Config";
|
||||||
|
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||||
|
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||||
|
import { FormattedMessage } from "../webviewContract";
|
||||||
|
import {
|
||||||
|
DebugSessionVisualizationSupport,
|
||||||
|
GetVisualizationDataArgs,
|
||||||
|
VisualizationBackend,
|
||||||
|
VisualizationBackendBase,
|
||||||
|
} from "./VisualizationBackend";
|
||||||
|
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
|
||||||
|
|
||||||
|
registerUpdateReconciler(module);
|
||||||
|
|
||||||
|
@hotClass(module)
|
||||||
|
export class PyEvaluationEngine implements DebugSessionVisualizationSupport {
|
||||||
|
constructor(
|
||||||
|
private readonly debuggerView: DebuggerViewProxy,
|
||||||
|
private readonly config: Config
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createBackend(
|
||||||
|
session: DebugSessionProxy
|
||||||
|
): VisualizationBackend | undefined {
|
||||||
|
const supportedDebugAdapters = ["python", "debugpy"];
|
||||||
|
|
||||||
|
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
||||||
|
return new PyVisualizationBackend(
|
||||||
|
session,
|
||||||
|
this.debuggerView,
|
||||||
|
this.config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PyVisualizationBackend extends VisualizationBackendBase {
|
||||||
|
public readonly expressionLanguageId = "python";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
debugSession: DebugSessionProxy,
|
||||||
|
debuggerView: DebuggerViewProxy,
|
||||||
|
private readonly config: Config
|
||||||
|
) {
|
||||||
|
super(debugSession, debuggerView);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getContext(): "watch" | "repl" {
|
||||||
|
// we will use "repl" as default so that results are not truncated.
|
||||||
|
return "repl";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVisualizationData({
|
||||||
|
expression,
|
||||||
|
preferredExtractorId,
|
||||||
|
}: GetVisualizationDataArgs): Promise<
|
||||||
|
| { kind: "data"; result: DataExtractionResult }
|
||||||
|
| { kind: "error"; message: FormattedMessage }
|
||||||
|
> {
|
||||||
|
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||||
|
this.debugSession
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalExpression = this.getFinalExpression({
|
||||||
|
expression,
|
||||||
|
preferredExtractorId,
|
||||||
|
});
|
||||||
|
let reply: { result: string; variablesReference: number };
|
||||||
|
try {
|
||||||
|
// inject vscodedebugvisualizer for python
|
||||||
|
await this.debugSession.evaluate({
|
||||||
|
expression:
|
||||||
|
"from vscodedebugvisualizer import visualize\ntry:\n import debugvisualizer\nexcept ImportError:\n pass",
|
||||||
|
frameId,
|
||||||
|
context: this.getContext(),
|
||||||
|
});
|
||||||
|
|
||||||
|
reply = await this.debugSession.evaluate({
|
||||||
|
expression: finalExpression,
|
||||||
|
frameId,
|
||||||
|
context: this.getContext(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = reply.result;
|
||||||
|
// remove the initial escape by the the debug session e.g. `''{"kind": {"text": true}, "text": "{"asdf1\'"}''`
|
||||||
|
result = result.replace(/\\'/g, "'");
|
||||||
|
result = result.replace(/\\\\/g, "\\");
|
||||||
|
|
||||||
|
return parseEvaluationResultFromGenericDebugAdapter(result, {
|
||||||
|
debugAdapterType: this.debugSession.session.configuration.type,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
let errorTyped = error as Error;
|
||||||
|
if (
|
||||||
|
errorTyped.message.includes(
|
||||||
|
"ModuleNotFoundError: No module named 'vscodedebugvisualizer'"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
message: {
|
||||||
|
kind: "list",
|
||||||
|
items: [
|
||||||
|
"Please make sure vscodedebugvisualizer is installed: `pip install vscodedebugvisualizer`",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
message: {
|
||||||
|
kind: "list",
|
||||||
|
items: [
|
||||||
|
"An error occurred while evaluating the expression:",
|
||||||
|
errorTyped.message,
|
||||||
|
`Used debug adapter: ${this.debugSession.session.configuration.type}`,
|
||||||
|
{
|
||||||
|
kind: "inlineList",
|
||||||
|
items: [
|
||||||
|
"Evaluated expression is",
|
||||||
|
{ kind: "code", content: finalExpression },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFinalExpression(args: {
|
||||||
|
expression: string;
|
||||||
|
preferredExtractorId: DataExtractorId | undefined;
|
||||||
|
}): string {
|
||||||
|
// wrap expression with visualize function
|
||||||
|
let pythonInject = "";
|
||||||
|
pythonInject += "visualize(" + args.expression + ")";
|
||||||
|
return pythonInject;
|
||||||
|
}
|
||||||
|
}
|
||||||
157
extension/src/VisualizationBackend/RbVisualizationSupport.ts
Normal file
157
extension/src/VisualizationBackend/RbVisualizationSupport.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import {
|
||||||
|
DataExtractionResult,
|
||||||
|
DataResult,
|
||||||
|
} from "@hediet/debug-visualizer-data-extraction";
|
||||||
|
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||||
|
import { Config } from "../Config";
|
||||||
|
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||||
|
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||||
|
import { FormattedMessage } from "../webviewContract";
|
||||||
|
import {
|
||||||
|
DebugSessionVisualizationSupport,
|
||||||
|
GetVisualizationDataArgs,
|
||||||
|
VisualizationBackend,
|
||||||
|
VisualizationBackendBase,
|
||||||
|
} from "./VisualizationBackend";
|
||||||
|
|
||||||
|
registerUpdateReconciler(module);
|
||||||
|
|
||||||
|
@hotClass(module)
|
||||||
|
export class RbEvaluationEngine implements DebugSessionVisualizationSupport {
|
||||||
|
constructor(
|
||||||
|
private readonly debuggerView: DebuggerViewProxy,
|
||||||
|
private readonly config: Config
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createBackend(
|
||||||
|
session: DebugSessionProxy
|
||||||
|
): VisualizationBackend | undefined {
|
||||||
|
const supportedDebugAdapters = ["rdbg"];
|
||||||
|
|
||||||
|
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
||||||
|
return new RbVisualizationBackend(
|
||||||
|
session,
|
||||||
|
this.debuggerView,
|
||||||
|
this.config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RbVisualizationBackend extends VisualizationBackendBase {
|
||||||
|
public readonly expressionLanguageId = "ruby";
|
||||||
|
constructor(
|
||||||
|
debugSession: DebugSessionProxy,
|
||||||
|
debuggerView: DebuggerViewProxy,
|
||||||
|
private readonly config: Config
|
||||||
|
) {
|
||||||
|
super(debugSession, debuggerView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly defaultContext = "repl";
|
||||||
|
|
||||||
|
public async getVisualizationData(
|
||||||
|
args: GetVisualizationDataArgs
|
||||||
|
): Promise<
|
||||||
|
| { kind: "data"; result: DataExtractionResult }
|
||||||
|
| { kind: "error"; message: FormattedMessage }
|
||||||
|
> {
|
||||||
|
const result = await this._getVisualizationData(args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getVisualizationData({
|
||||||
|
expression,
|
||||||
|
preferredExtractorId,
|
||||||
|
}: GetVisualizationDataArgs): Promise<
|
||||||
|
| { kind: "data"; result: DataExtractionResult }
|
||||||
|
| { kind: "error"; message: FormattedMessage }
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
if (expression.length === 0) throw new Error("No extractors");
|
||||||
|
|
||||||
|
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||||
|
this.debugSession
|
||||||
|
);
|
||||||
|
const initialReply = await this.debugSession.evaluate({
|
||||||
|
expression: "require 'debugvisualizer'",
|
||||||
|
frameId,
|
||||||
|
context: this.defaultContext,
|
||||||
|
});
|
||||||
|
if (initialReply.result.includes("LoadError")) {
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
message: {
|
||||||
|
kind: "list",
|
||||||
|
items: [
|
||||||
|
"LoadError: Failed to load debugvisualizer.",
|
||||||
|
{
|
||||||
|
kind: "inlineList",
|
||||||
|
items: [
|
||||||
|
"Install the gem by executing:",
|
||||||
|
{
|
||||||
|
kind: "code",
|
||||||
|
content: "$ bundle add debugvisualizer",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "inlineList",
|
||||||
|
items: [
|
||||||
|
"If bundler is not being used, install the gem by executing:",
|
||||||
|
{
|
||||||
|
kind: "code",
|
||||||
|
content:
|
||||||
|
"$ gem install debugvisualizer",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const preferredId = preferredExtractorId || "";
|
||||||
|
const wrappedExpr = `
|
||||||
|
DebugVisualizer.to_debug_visualizer_protocol_json("${preferredId}", ${expression})
|
||||||
|
`;
|
||||||
|
const reply = await this.debugSession.evaluate({
|
||||||
|
expression: wrappedExpr,
|
||||||
|
frameId,
|
||||||
|
context: this.defaultContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
let dataResult: DataResult;
|
||||||
|
try {
|
||||||
|
// Debuggee converts result to a JSON string twice.
|
||||||
|
dataResult = JSON.parse(JSON.parse(reply.result)) as DataResult;
|
||||||
|
} catch (error: any) {
|
||||||
|
let message = error.message;
|
||||||
|
// The `reply.result` is as follows when error occurs in the evaluation of an expression and parsing will fail.
|
||||||
|
// e.g. "#<ZeroDivisionError: divided by 0>"
|
||||||
|
// In this case, it is more beneficial to display this error message.
|
||||||
|
if (reply.result.includes("Error")) message = reply.result;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (dataResult.kind) {
|
||||||
|
case "NoExtractors":
|
||||||
|
throw new Error("No extractors");
|
||||||
|
case "Error":
|
||||||
|
throw new Error(dataResult.message);
|
||||||
|
case "Data":
|
||||||
|
return {
|
||||||
|
kind: "data",
|
||||||
|
result: dataResult.extractionResult,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid Data");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
DataExtractorId,
|
DataExtractorId,
|
||||||
} from "@hediet/debug-visualizer-data-extraction";
|
} from "@hediet/debug-visualizer-data-extraction";
|
||||||
import { Disposable } from "@hediet/std/disposable";
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
|
||||||
import { CompletionItem, FormattedMessage } from "../webviewContract";
|
|
||||||
import { reaction } from "mobx";
|
|
||||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
|
||||||
import { EventEmitter, EventSource } from "@hediet/std/events";
|
import { EventEmitter, EventSource } from "@hediet/std/events";
|
||||||
|
import { reaction } from "mobx";
|
||||||
|
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||||
|
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||||
|
import { CompletionItem, FormattedMessage } from "../webviewContract";
|
||||||
|
|
||||||
export interface DebugSessionVisualizationSupport {
|
export interface DebugSessionVisualizationSupport {
|
||||||
createBackend(session: DebugSessionProxy): VisualizationBackend | undefined;
|
createBackend(session: DebugSessionProxy): VisualizationBackend | undefined;
|
||||||
@@ -29,11 +29,22 @@ export interface VisualizationBackend extends Disposable {
|
|||||||
readonly expressionLanguageId: string | undefined;
|
readonly expressionLanguageId: string | undefined;
|
||||||
|
|
||||||
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates `expression` in the current frame for its side effect
|
||||||
|
* (e.g. an assignment) rather than to extract visualization data.
|
||||||
|
*/
|
||||||
|
evaluateForEffect(
|
||||||
|
expression: string
|
||||||
|
): Promise<{ result: string } | { error: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetVisualizationDataArgs {
|
export interface GetVisualizationDataArgs {
|
||||||
expression: string;
|
readonly expression: string;
|
||||||
preferredExtractorId: DataExtractorId | undefined;
|
readonly preferredExtractorId: DataExtractorId | undefined;
|
||||||
|
|
||||||
|
// Can be used to attach data to the session.
|
||||||
|
readonly sessionStore: { data: unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class VisualizationBackendBase implements VisualizationBackend {
|
export abstract class VisualizationBackendBase implements VisualizationBackend {
|
||||||
@@ -49,7 +60,7 @@ export abstract class VisualizationBackendBase implements VisualizationBackend {
|
|||||||
this.dispose.track({
|
this.dispose.track({
|
||||||
dispose: reaction(
|
dispose: reaction(
|
||||||
() => debuggerView.getActiveStackFrameId(debugSession),
|
() => debuggerView.getActiveStackFrameId(debugSession),
|
||||||
activeStackFrameId => {
|
(activeStackFrameId) => {
|
||||||
if (activeStackFrameId !== undefined) {
|
if (activeStackFrameId !== undefined) {
|
||||||
this.onChangeEmitter.emit();
|
this.onChangeEmitter.emit();
|
||||||
}
|
}
|
||||||
@@ -77,4 +88,22 @@ export abstract class VisualizationBackendBase implements VisualizationBackend {
|
|||||||
column,
|
column,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async evaluateForEffect(
|
||||||
|
expression: string
|
||||||
|
): Promise<{ result: string } | { error: string }> {
|
||||||
|
try {
|
||||||
|
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||||
|
this.debugSession
|
||||||
|
);
|
||||||
|
const reply = await this.debugSession.evaluate({
|
||||||
|
expression,
|
||||||
|
frameId,
|
||||||
|
context: "repl",
|
||||||
|
});
|
||||||
|
return { result: reply.result };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,8 @@ export * from "./VisualizationBackend";
|
|||||||
|
|
||||||
export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport";
|
export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport";
|
||||||
export { JsEvaluationEngine } from "./JsVisualizationSupport";
|
export { JsEvaluationEngine } from "./JsVisualizationSupport";
|
||||||
|
export { PyEvaluationEngine } from "./PyVisualizationSupport";
|
||||||
|
export { RbEvaluationEngine } from './RbVisualizationSupport';
|
||||||
export { GenericVisualizationSupport } from "./GenericVisualizationSupport";
|
export { GenericVisualizationSupport } from "./GenericVisualizationSupport";
|
||||||
export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport";
|
export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport";
|
||||||
|
export { NodeTableGraphEngine } from "./NodeTableGraphVisualizationSupport";
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function parseEvaluationResultFromGenericDebugAdapter(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
return {
|
return {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
@@ -93,7 +93,7 @@ function isEnclosedWith(str: string, char: string): boolean {
|
|||||||
function parseJson(str: string, context: ParseEvaluationResultContext) {
|
function parseJson(str: string, context: ParseEvaluationResultContext) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str);
|
return JSON.parse(str);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
throw new FormattedError({
|
throw new FormattedError({
|
||||||
kind: "list",
|
kind: "list",
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||||
import { DataExtractionState, CompletionItem } from "../webviewContract";
|
import { CompletionItem, DataExtractionState } from "../webviewContract";
|
||||||
|
|
||||||
export interface VisualizationWatchModel {
|
export interface VisualizationWatchModel {
|
||||||
createWatch(
|
createWatch(
|
||||||
@@ -15,10 +15,19 @@ export interface VisualizationWatchModel {
|
|||||||
* This field is observable.
|
* This field is observable.
|
||||||
*/
|
*/
|
||||||
readonly languageId: string | undefined;
|
readonly languageId: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates `expression` in the current frame for its side effect
|
||||||
|
* (e.g. an assignment) rather than to extract visualization data.
|
||||||
|
*/
|
||||||
|
evaluateForEffect(
|
||||||
|
expression: string
|
||||||
|
): Promise<{ result: string } | { error: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisualizationWatchOptions {
|
export interface VisualizationWatchOptions {
|
||||||
preferredDataExtractor?: DataExtractorId | undefined;
|
preferredDataExtractor?: DataExtractorId | undefined;
|
||||||
|
sessionState?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisualizationWatch {
|
export interface VisualizationWatch {
|
||||||
@@ -31,6 +40,8 @@ export interface VisualizationWatch {
|
|||||||
/** This field is observable */
|
/** This field is observable */
|
||||||
readonly preferredDataExtractor: DataExtractorId | undefined;
|
readonly preferredDataExtractor: DataExtractorId | undefined;
|
||||||
|
|
||||||
|
readonly sessionState: unknown;
|
||||||
|
|
||||||
setPreferredDataExtractor(id: DataExtractorId | undefined): void;
|
setPreferredDataExtractor(id: DataExtractorId | undefined): void;
|
||||||
refresh(): void;
|
refresh(): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ import { action, observable } from "mobx";
|
|||||||
import { CancellationToken, CancellationTokenSource } from "vscode";
|
import { CancellationToken, CancellationTokenSource } from "vscode";
|
||||||
import { VisualizationBackend } from "../VisualizationBackend/VisualizationBackend";
|
import { VisualizationBackend } from "../VisualizationBackend/VisualizationBackend";
|
||||||
import { CompletionItem, DataExtractionState } from "../webviewContract";
|
import { CompletionItem, DataExtractionState } from "../webviewContract";
|
||||||
import {
|
import { VisualizationWatch, VisualizationWatchModel, VisualizationWatchOptions } from "./VisualizationWatchModel";
|
||||||
VisualizationWatch,
|
|
||||||
VisualizationWatchModel,
|
|
||||||
VisualizationWatchOptions,
|
|
||||||
} from "./VisualizationWatchModel";
|
|
||||||
|
|
||||||
@hotClass(module)
|
@hotClass(module)
|
||||||
export class VisualizationWatchModelImpl implements VisualizationWatchModel {
|
export class VisualizationWatchModelImpl implements VisualizationWatchModel {
|
||||||
@@ -22,23 +18,15 @@ export class VisualizationWatchModelImpl implements VisualizationWatchModel {
|
|||||||
this.dispose.track(
|
this.dispose.track(
|
||||||
visualizationBackend.onChange.sub(() => {
|
visualizationBackend.onChange.sub(() => {
|
||||||
if (visualizationBackend.expressionLanguageId !== undefined) {
|
if (visualizationBackend.expressionLanguageId !== undefined) {
|
||||||
this.lastLanguageId =
|
this.lastLanguageId = visualizationBackend.expressionLanguageId;
|
||||||
visualizationBackend.expressionLanguageId;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.lastLanguageId = visualizationBackend.expressionLanguageId;
|
this.lastLanguageId = visualizationBackend.expressionLanguageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public createWatch(
|
public createWatch(expression: string, options: VisualizationWatchOptions): VisualizationWatch {
|
||||||
expression: string,
|
const w = new ObservableVisualizationWatch(expression, options, this.visualizationBackend);
|
||||||
options: VisualizationWatchOptions
|
|
||||||
): VisualizationWatch {
|
|
||||||
const w = new ObservableVisualizationWatch(
|
|
||||||
expression,
|
|
||||||
options,
|
|
||||||
this.visualizationBackend
|
|
||||||
);
|
|
||||||
w.onDispose.sub(() => {
|
w.onDispose.sub(() => {
|
||||||
this.watchers.delete(w);
|
this.watchers.delete(w);
|
||||||
});
|
});
|
||||||
@@ -53,12 +41,15 @@ export class VisualizationWatchModelImpl implements VisualizationWatchModel {
|
|||||||
return this.lastLanguageId;
|
return this.lastLanguageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCompletions(
|
public getCompletions(text: string, column: number): Promise<CompletionItem[]> {
|
||||||
text: string,
|
|
||||||
column: number
|
|
||||||
): Promise<CompletionItem[]> {
|
|
||||||
return this.visualizationBackend.getCompletions(text, column);
|
return this.visualizationBackend.getCompletions(text, column);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public evaluateForEffect(
|
||||||
|
expression: string
|
||||||
|
): Promise<{ result: string } | { error: string }> {
|
||||||
|
return this.visualizationBackend.evaluateForEffect(expression);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObservableVisualizationWatch implements VisualizationWatch {
|
class ObservableVisualizationWatch implements VisualizationWatch {
|
||||||
@@ -115,31 +106,22 @@ class ObservableVisualizationWatch implements VisualizationWatch {
|
|||||||
this._refresh(tokenSource.token);
|
this._refresh(tokenSource.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sessionState: unknown = undefined;
|
||||||
|
|
||||||
private async _refresh(token: CancellationToken): Promise<void> {
|
private async _refresh(token: CancellationToken): Promise<void> {
|
||||||
/*const session = this.debuggerView.activeDebugSession;
|
|
||||||
if (!session) {
|
|
||||||
this._state = { kind: "noDebugSession" };
|
|
||||||
return;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
//const frameId = this.debuggerView.activeFrameId;
|
|
||||||
|
|
||||||
this._state = { kind: "loading" };
|
this._state = { kind: "loading" };
|
||||||
|
const that = this;
|
||||||
/*
|
|
||||||
const visualizationBackend = this.getVisualizationBackend();
|
|
||||||
if (!visualizationBackend) {
|
|
||||||
this._state = {
|
|
||||||
kind: "error",
|
|
||||||
message: `The debug adapter "${session.session.type}" is not supported.`,
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const result = await this.visualizationBackend.getVisualizationData({
|
const result = await this.visualizationBackend.getVisualizationData({
|
||||||
expression: this.expression,
|
expression: this.expression,
|
||||||
//frameId,
|
|
||||||
preferredExtractorId: this.preferredDataExtractor,
|
preferredExtractorId: this.preferredDataExtractor,
|
||||||
|
sessionStore: {
|
||||||
|
get data(): unknown {
|
||||||
|
return that.sessionState;
|
||||||
|
},
|
||||||
|
set data(value: unknown) {
|
||||||
|
that.sessionState = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.kind === "error") {
|
if (result.kind === "error") {
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ import { VisualizationWatchModelImpl } from "./VisualizationWatchModel";
|
|||||||
import {
|
import {
|
||||||
ComposedVisualizationSupport,
|
ComposedVisualizationSupport,
|
||||||
JsEvaluationEngine,
|
JsEvaluationEngine,
|
||||||
|
PyEvaluationEngine,
|
||||||
|
RbEvaluationEngine,
|
||||||
GenericVisualizationSupport,
|
GenericVisualizationSupport,
|
||||||
ConfigurableVisualizationSupport,
|
ConfigurableVisualizationSupport,
|
||||||
|
NodeTableGraphEngine,
|
||||||
} from "./VisualizationBackend";
|
} from "./VisualizationBackend";
|
||||||
import { DispatchingVisualizationBackend } from "./VisualizationBackend/DispatchingVisualizationBackend";
|
import { DispatchingVisualizationBackend } from "./VisualizationBackend/DispatchingVisualizationBackend";
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ export function activate(context: ExtensionContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {}
|
export function deactivate() { }
|
||||||
|
|
||||||
export class Extension {
|
export class Extension {
|
||||||
public readonly dispose = Disposable.fn();
|
public readonly dispose = Disposable.fn();
|
||||||
@@ -46,11 +49,14 @@ export class Extension {
|
|||||||
public readonly dataSource = new VisualizationWatchModelImpl(
|
public readonly dataSource = new VisualizationWatchModelImpl(
|
||||||
new DispatchingVisualizationBackend(
|
new DispatchingVisualizationBackend(
|
||||||
new ComposedVisualizationSupport([
|
new ComposedVisualizationSupport([
|
||||||
|
new NodeTableGraphEngine(this.debuggerView, this.config),
|
||||||
new ConfigurableVisualizationSupport(
|
new ConfigurableVisualizationSupport(
|
||||||
this.config,
|
this.config,
|
||||||
this.debuggerView
|
this.debuggerView
|
||||||
),
|
),
|
||||||
new JsEvaluationEngine(this.debuggerView, this.config),
|
new JsEvaluationEngine(this.debuggerView, this.config),
|
||||||
|
new PyEvaluationEngine(this.debuggerView, this.config),
|
||||||
|
new RbEvaluationEngine(this.debuggerView, this.config),
|
||||||
new GenericVisualizationSupport(this.debuggerView),
|
new GenericVisualizationSupport(this.debuggerView),
|
||||||
]),
|
]),
|
||||||
this.debuggerView
|
this.debuggerView
|
||||||
@@ -100,7 +106,7 @@ export class Extension {
|
|||||||
if (
|
if (
|
||||||
match.index <= selection.start.character &&
|
match.index <= selection.start.character &&
|
||||||
selection.start.character <=
|
selection.start.character <=
|
||||||
match.index + match[0].length
|
match.index + match[0].length
|
||||||
) {
|
) {
|
||||||
selectedText = match[1];
|
selectedText = match[1];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
import { DebugSession } from "vscode";
|
import { DebugSession } from "vscode";
|
||||||
import { CompletionItem } from "../webviewContract";
|
import { CompletionItem } from "../webviewContract";
|
||||||
import { observable } from "mobx";
|
|
||||||
|
|
||||||
export class DebugSessionProxy {
|
export class DebugSessionProxy {
|
||||||
@observable private _activeStackFrameId: number | undefined;
|
@observable private _activeStackFrameId: number | undefined;
|
||||||
|
|
||||||
constructor(public readonly session: DebugSession) {}
|
constructor(public readonly session: DebugSession) {}
|
||||||
|
|
||||||
protected async getStackTrace(args: {
|
public async getStackTrace(args: {
|
||||||
threadId: number;
|
threadId: number;
|
||||||
startFrame?: number;
|
startFrame?: number;
|
||||||
levels?: number;
|
levels?: number;
|
||||||
}): Promise<{ totalFrames?: number; stackFrames: StackFrame[] }> {
|
}): Promise<StackTraceInfo> {
|
||||||
try {
|
try {
|
||||||
const reply = (await this.session.customRequest("stackTrace", {
|
const reply = (await this.session.customRequest("stackTrace", {
|
||||||
threadId: args.threadId,
|
threadId: args.threadId,
|
||||||
@@ -61,9 +61,7 @@ export class DebugSessionProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getVariables(args: {
|
public async getVariables(args: { variablesReference: number }): Promise<Variable[]> {
|
||||||
variablesReference: number;
|
|
||||||
}): Promise<Variable[]> {
|
|
||||||
try {
|
try {
|
||||||
const reply = await this.session.customRequest("variables", {
|
const reply = await this.session.customRequest("variables", {
|
||||||
variablesReference: args.variablesReference,
|
variablesReference: args.variablesReference,
|
||||||
@@ -87,7 +85,7 @@ export class DebugSessionProxy {
|
|||||||
expression: string;
|
expression: string;
|
||||||
frameId: number | undefined;
|
frameId: number | undefined;
|
||||||
context: "watch" | "repl" | "copy";
|
context: "watch" | "repl" | "copy";
|
||||||
}): Promise<{ result: string; variablesReference: number }> {
|
}): Promise<{ result: string; variablesReference: number; type?: string }> {
|
||||||
const reply = await this.session.customRequest("evaluate", {
|
const reply = await this.session.customRequest("evaluate", {
|
||||||
expression: args.expression,
|
expression: args.expression,
|
||||||
frameId: args.frameId,
|
frameId: args.frameId,
|
||||||
@@ -96,10 +94,16 @@ export class DebugSessionProxy {
|
|||||||
return {
|
return {
|
||||||
result: reply.result,
|
result: reply.result,
|
||||||
variablesReference: reply.variablesReference,
|
variablesReference: reply.variablesReference,
|
||||||
|
type: reply.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StackTraceInfo {
|
||||||
|
totalFrames?: number;
|
||||||
|
stackFrames: StackFrame[];
|
||||||
|
}
|
||||||
|
|
||||||
interface Scope {
|
interface Scope {
|
||||||
name: string;
|
name: string;
|
||||||
expensive: boolean;
|
expensive: boolean;
|
||||||
@@ -110,9 +114,11 @@ interface Variable {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
variablesReference: number;
|
variablesReference: number;
|
||||||
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StackFrame {
|
export interface StackFrame {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
source: { name: string; path: string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Disposable } from "@hediet/std/disposable";
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
import { debug, DebugSession } from "vscode";
|
|
||||||
import { EventEmitter } from "@hediet/std/events";
|
import { EventEmitter } from "@hediet/std/events";
|
||||||
import { runInAction } from "mobx";
|
import { debug, DebugSession } from "vscode";
|
||||||
import { DebugSessionProxy } from "./DebugSessionProxy";
|
import { DebugSessionProxy } from "./DebugSessionProxy";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +13,8 @@ export class DebuggerProxy {
|
|||||||
private readonly _onDidStartDebugSession = new EventEmitter<{
|
private readonly _onDidStartDebugSession = new EventEmitter<{
|
||||||
session: DebugSessionProxy;
|
session: DebugSessionProxy;
|
||||||
}>();
|
}>();
|
||||||
public readonly onDidStartDebugSession = this._onDidStartDebugSession.asEvent();
|
public readonly onDidStartDebugSession =
|
||||||
|
this._onDidStartDebugSession.asEvent();
|
||||||
|
|
||||||
public getDebugSessionProxy(session: DebugSession): DebugSessionProxy {
|
public getDebugSessionProxy(session: DebugSession): DebugSessionProxy {
|
||||||
let result = this.sessions.get(session);
|
let result = this.sessions.get(session);
|
||||||
@@ -27,18 +27,18 @@ export class DebuggerProxy {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dispose.track([
|
this.dispose.track([
|
||||||
debug.onDidStartDebugSession(session => {
|
debug.onDidStartDebugSession((session) => {
|
||||||
const e = this.sessions.get(session)!;
|
const e = this.sessions.get(session)!;
|
||||||
this._onDidStartDebugSession.emit({ session: e });
|
this._onDidStartDebugSession.emit({ session: e });
|
||||||
}),
|
}),
|
||||||
debug.onDidTerminateDebugSession(session => {
|
debug.onDidTerminateDebugSession((session) => {
|
||||||
this.sessions.delete(session);
|
this.sessions.delete(session);
|
||||||
}),
|
}),
|
||||||
debug.registerDebugAdapterTrackerFactory("*", {
|
debug.registerDebugAdapterTrackerFactory("*", {
|
||||||
createDebugAdapterTracker: session => {
|
createDebugAdapterTracker: (session) => {
|
||||||
const extendedSession = this.getDebugSessionProxy(session);
|
const extendedSession = this.getDebugSessionProxy(session);
|
||||||
return {
|
return {
|
||||||
onDidSendMessage: async msg => {
|
onDidSendMessage: async (msg) => {
|
||||||
type Message =
|
type Message =
|
||||||
| StoppedEvent
|
| StoppedEvent
|
||||||
| ThreadsResponse
|
| ThreadsResponse
|
||||||
@@ -79,13 +79,12 @@ export class DebuggerProxy {
|
|||||||
if (m.type === "event") {
|
if (m.type === "event") {
|
||||||
if (m.event === "stopped") {
|
if (m.event === "stopped") {
|
||||||
const threadId = m.body.threadId;
|
const threadId = m.body.threadId;
|
||||||
const r = await extendedSession[
|
const r =
|
||||||
"getStackTrace"
|
await extendedSession.getStackTrace({
|
||||||
]({
|
threadId,
|
||||||
threadId,
|
startFrame: 0,
|
||||||
startFrame: 0,
|
levels: 1,
|
||||||
levels: 1,
|
});
|
||||||
});
|
|
||||||
extendedSession["_activeStackFrameId"] =
|
extendedSession["_activeStackFrameId"] =
|
||||||
r.stackFrames.length > 0
|
r.stackFrames.length > 0
|
||||||
? r.stackFrames[0].id
|
? r.stackFrames[0].id
|
||||||
@@ -98,9 +97,8 @@ export class DebuggerProxy {
|
|||||||
m.command === "stepIn" ||
|
m.command === "stepIn" ||
|
||||||
m.command === "stepOut"
|
m.command === "stepOut"
|
||||||
) {
|
) {
|
||||||
extendedSession[
|
extendedSession["_activeStackFrameId"] =
|
||||||
"_activeStackFrameId"
|
undefined;
|
||||||
] = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Disposable } from "@hediet/std/disposable";
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
import { debug, DebugSession } from "vscode";
|
import { CancellationToken, debug, DebugSession, InlineValue, InlineValueContext, InlineValuesProvider, languages, ProviderResult, Range, TextDocument } from "vscode";
|
||||||
import { observable, action } from "mobx";
|
import { observable, action } from "mobx";
|
||||||
import { DebuggerProxy } from "./DebuggerProxy";
|
import { DebuggerProxy } from "./DebuggerProxy";
|
||||||
import { DebugSessionProxy } from "./DebugSessionProxy";
|
import { DebugSessionProxy } from "./DebugSessionProxy";
|
||||||
@@ -30,6 +30,8 @@ export class DebuggerViewProxy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.updateActiveDebugSession(debug.activeDebugSession);
|
this.updateActiveDebugSession(debug.activeDebugSession);
|
||||||
|
|
||||||
|
this.dispose.track(languages.registerInlineValuesProvider('*', new FrameIdGetter(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -39,3 +41,18 @@ export class DebuggerViewProxy {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FrameIdGetter implements InlineValuesProvider {
|
||||||
|
constructor(
|
||||||
|
private readonly debuggerViewProxy: DebuggerViewProxy,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): ProviderResult<InlineValue[]> {
|
||||||
|
const a = this.debuggerViewProxy.activeDebugSession;
|
||||||
|
if (a) {
|
||||||
|
a['_activeStackFrameId'] = context.frameId;
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
21
extension/src/utils/DebouncedRunner.ts
Normal file
21
extension/src/utils/DebouncedRunner.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export class DebouncedRunner {
|
||||||
|
private timeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
constructor(private readonly debounceTimeout: number) {}
|
||||||
|
|
||||||
|
public run(action: () => void): void {
|
||||||
|
this.clear();
|
||||||
|
this.timeout = setTimeout(action, this.debounceTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clear() {
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
extension/src/utils/IncrementalMap.ts
Normal file
30
extension/src/utils/IncrementalMap.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
|
import { observable, autorun } from "mobx";
|
||||||
|
|
||||||
|
export class IncrementalMap<TKey, TValue extends Disposable> {
|
||||||
|
public readonly map: Map<TKey, TValue> = new Map();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly getKeys: () => TKey[],
|
||||||
|
private readonly getValue: (key: TKey) => TValue
|
||||||
|
) {
|
||||||
|
this.dispose.track({
|
||||||
|
dispose: autorun(() => {
|
||||||
|
const keys = this.getKeys();
|
||||||
|
const toRemove = new Set(this.map.keys());
|
||||||
|
for (const key of keys) {
|
||||||
|
toRemove.delete(key);
|
||||||
|
if (!this.map.has(key)) {
|
||||||
|
this.map.set(key, this.getValue(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of toRemove) {
|
||||||
|
this.map.get(key)!.dispose();
|
||||||
|
this.map.delete(key);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly dispose = Disposable.fn();
|
||||||
|
}
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
import { Disposable } from "@hediet/std/disposable";
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
import { webviewContract } from "../webviewContract";
|
|
||||||
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
|
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
|
||||||
import {
|
|
||||||
VisualizationWatch,
|
|
||||||
VisualizationWatchModel,
|
|
||||||
} from "../VisualizationWatchModel";
|
|
||||||
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
||||||
import { observable, autorun } from "mobx";
|
import { existsSync, readFileSync, watch } from "fs";
|
||||||
import { WebviewServer } from "./WebviewServer";
|
import { autorun, observable } from "mobx";
|
||||||
import * as open from "open";
|
import * as open from "open";
|
||||||
import chromeLauncher = require("chrome-launcher");
|
import { window } from "vscode";
|
||||||
import { Config } from "../Config";
|
import { Config } from "../Config";
|
||||||
|
import { VisualizationWatch, VisualizationWatchModel } from "../VisualizationWatchModel";
|
||||||
|
import { DebouncedRunner } from "../utils/DebouncedRunner";
|
||||||
|
import { IncrementalMap } from "../utils/IncrementalMap";
|
||||||
|
import { webviewContract } from "../webviewContract";
|
||||||
|
import { WebviewServer } from "./WebviewServer";
|
||||||
|
import chromeLauncher = require("chrome-launcher");
|
||||||
|
|
||||||
export class WebviewConnection {
|
export class WebviewConnection {
|
||||||
public readonly dispose = Disposable.fn();
|
public readonly dispose = Disposable.fn();
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
private watcher: VisualizationWatch | undefined = undefined;
|
private watcher: VisualizationWatch | undefined = undefined;
|
||||||
|
|
||||||
private readonly client: typeof webviewContract["TClientInterface"];
|
private readonly client: (typeof webviewContract)["TClientInterface"];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
evaluationWatchService: VisualizationWatchModel,
|
evaluationWatchService: VisualizationWatchModel,
|
||||||
@@ -34,80 +36,113 @@ export class WebviewConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { client, channel } = webviewContract.registerServerToStream(
|
const { client, channel } = webviewContract.registerServerToStream(stream, new ConsoleRpcLogger(), {
|
||||||
stream,
|
authenticate: async ({ secret }, { newErr }) => {
|
||||||
new ConsoleRpcLogger(),
|
if (secret !== serverSecret) {
|
||||||
{
|
return newErr({ errorMessage: "Invalid Secret" });
|
||||||
authenticate: async ({ secret }, { newErr }) => {
|
} else {
|
||||||
if (secret !== serverSecret) {
|
authenticated = true;
|
||||||
return newErr({ errorMessage: "Invalid Secret" });
|
}
|
||||||
} else {
|
},
|
||||||
authenticated = true;
|
refresh: async () => {
|
||||||
}
|
throwIfNotAuthenticated();
|
||||||
},
|
|
||||||
refresh: async () => {
|
|
||||||
throwIfNotAuthenticated();
|
|
||||||
|
|
||||||
if (this.watcher) {
|
if (this.watcher) {
|
||||||
this.watcher.refresh();
|
this.watcher.refresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setExpression: async ({ newExpression }) => {
|
setExpression: async ({ newExpression }) => {
|
||||||
throwIfNotAuthenticated();
|
throwIfNotAuthenticated();
|
||||||
|
|
||||||
let oldPreferredDataExtractor: VisualizationWatch["preferredDataExtractor"];
|
if (this.watcher?.expression === newExpression) {
|
||||||
if (this.watcher) {
|
return;
|
||||||
oldPreferredDataExtractor = this.watcher
|
}
|
||||||
.preferredDataExtractor;
|
|
||||||
this.dispose.untrack(this.watcher).dispose();
|
|
||||||
}
|
|
||||||
this.watcher = this.dispose.track(
|
|
||||||
evaluationWatchService.createWatch(newExpression, {
|
|
||||||
preferredDataExtractor: oldPreferredDataExtractor,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
openInBrowser: async ({}) => {
|
|
||||||
throwIfNotAuthenticated();
|
|
||||||
|
|
||||||
const url = server.getWebviewPageUrl({
|
let oldPreferredDataExtractor: VisualizationWatch["preferredDataExtractor"];
|
||||||
mode: "standalone",
|
let oldSessionState: unknown;
|
||||||
expression: this.watcher
|
if (this.watcher) {
|
||||||
? this.watcher.expression
|
oldPreferredDataExtractor = this.watcher.preferredDataExtractor;
|
||||||
: undefined,
|
oldSessionState = this.watcher.sessionState;
|
||||||
});
|
this.dispose.untrack(this.watcher).dispose();
|
||||||
|
}
|
||||||
|
this.watcher = this.dispose.track(
|
||||||
|
evaluationWatchService.createWatch(newExpression, {
|
||||||
|
preferredDataExtractor: oldPreferredDataExtractor,
|
||||||
|
sessionState: oldSessionState,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
openInBrowser: async ({}) => {
|
||||||
|
throwIfNotAuthenticated();
|
||||||
|
|
||||||
let opened = false;
|
const url = server.getWebviewPageUrl({
|
||||||
if (config.useChromeKioskMode) {
|
mode: "standalone",
|
||||||
opened = await launchChrome(url);
|
expression: this.watcher ? this.watcher.expression : undefined,
|
||||||
}
|
});
|
||||||
if (!opened) {
|
|
||||||
open(url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setPreferredDataExtractor: async ({ dataExtractorId }) => {
|
|
||||||
throwIfNotAuthenticated();
|
|
||||||
|
|
||||||
if (this.watcher) {
|
let opened = false;
|
||||||
this.watcher.setPreferredDataExtractor(dataExtractorId);
|
if (config.useChromeKioskMode) {
|
||||||
}
|
opened = await launchChrome(url);
|
||||||
},
|
}
|
||||||
getCompletions: async ({ text, column }) => {
|
if (!opened) {
|
||||||
throwIfNotAuthenticated();
|
open(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPreferredDataExtractor: async ({ dataExtractorId }) => {
|
||||||
|
throwIfNotAuthenticated();
|
||||||
|
|
||||||
const completions = await evaluationWatchService.getCompletions(
|
if (this.watcher) {
|
||||||
text,
|
this.watcher.setPreferredDataExtractor(dataExtractorId);
|
||||||
column
|
}
|
||||||
);
|
},
|
||||||
return {
|
getCompletions: async ({ text, column }) => {
|
||||||
completions,
|
throwIfNotAuthenticated();
|
||||||
};
|
|
||||||
},
|
const completions = await evaluationWatchService.getCompletions(text, column);
|
||||||
}
|
return {
|
||||||
);
|
completions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
evaluateForEffect: async ({ expression }) => {
|
||||||
|
throwIfNotAuthenticated();
|
||||||
|
|
||||||
|
const reply = await evaluationWatchService.evaluateForEffect(expression);
|
||||||
|
return "error" in reply
|
||||||
|
? { success: false, message: reply.error }
|
||||||
|
: { success: true, message: reply.result };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
|
this.dispose.track(
|
||||||
|
new FileWatcher(
|
||||||
|
() => config.customVisualizerScriptPaths,
|
||||||
|
async (files) => {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.fileExists) {
|
||||||
|
window.showErrorMessage(`The file ${file.path} does not exist.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.setCustomVisualizerScript({
|
||||||
|
id: file.path,
|
||||||
|
jsSource: file.content || null,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
window.showErrorMessage(
|
||||||
|
'Error while running custom visualization extractor script "' +
|
||||||
|
file.path +
|
||||||
|
'": ' +
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
this.dispose.track([
|
this.dispose.track([
|
||||||
Disposable.create(
|
Disposable.create(
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
@@ -153,3 +188,88 @@ async function launchChrome(url: string): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FileWatcher {
|
||||||
|
public readonly dispose = Disposable.fn();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
getFilePaths: () => string[],
|
||||||
|
handleFileContents: (
|
||||||
|
files: {
|
||||||
|
path: string;
|
||||||
|
fileExists: boolean;
|
||||||
|
content: string | undefined;
|
||||||
|
}[]
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
const scheduleRefresh = new Set<SingleFileWatcher>();
|
||||||
|
const debouncer = new DebouncedRunner(200);
|
||||||
|
|
||||||
|
const map = this.dispose.track(
|
||||||
|
new IncrementalMap(
|
||||||
|
getFilePaths,
|
||||||
|
(path) =>
|
||||||
|
new SingleFileWatcher(path, (w) => {
|
||||||
|
scheduleRefresh.add(w);
|
||||||
|
debouncer.run(() => {
|
||||||
|
const changedWatchers = [...scheduleRefresh].filter((w) => w.refresh());
|
||||||
|
handleFileContents(
|
||||||
|
changedWatchers.map((w) => ({
|
||||||
|
path: w.path,
|
||||||
|
content: w.content,
|
||||||
|
fileExists: w.exists,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
handleFileContents(
|
||||||
|
[...map.map].map(([path, o]) => {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
content: o.content,
|
||||||
|
fileExists: o.exists,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SingleFileWatcher {
|
||||||
|
private readonly watcher = watch(this.path, { encoding: "utf-8" }, () => this.scheduleRefresh(this));
|
||||||
|
private isDisposed = false;
|
||||||
|
public content: string | undefined;
|
||||||
|
public exists: boolean = true;
|
||||||
|
|
||||||
|
constructor(public readonly path: string, private readonly scheduleRefresh: (self: SingleFileWatcher) => void) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.isDisposed = true;
|
||||||
|
this.content = undefined;
|
||||||
|
this.scheduleRefresh(this);
|
||||||
|
this.watcher.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public refresh() {
|
||||||
|
if (this.isDisposed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(this.path)) {
|
||||||
|
this.exists = false;
|
||||||
|
this.content = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = readFileSync(this.path, "utf-8");
|
||||||
|
if (newContent !== this.content) {
|
||||||
|
this.content = newContent;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ export const webviewContract = contract({
|
|||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setCustomVisualizerScript: notificationContract({
|
||||||
|
params: types.type({
|
||||||
|
id: types.string,
|
||||||
|
jsSource: types.union([types.string, types.null]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
authenticate: requestContract({
|
authenticate: requestContract({
|
||||||
@@ -83,6 +90,23 @@ export const webviewContract = contract({
|
|||||||
completions: types.array(unchecked<CompletionItem>()),
|
completions: types.array(unchecked<CompletionItem>()),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Evaluates `expression` for its side effect (e.g. an assignment
|
||||||
|
// like `x = 5`) in the active debug session's current frame, using
|
||||||
|
// the same DebugSessionProxy.evaluate() the read-only visualization
|
||||||
|
// path already uses (context "repl", so debug adapters that only
|
||||||
|
// truncate/sanitize "watch" context results still run this in full).
|
||||||
|
// Used by custom visualizer scripts (see vis-plugins/node-table-
|
||||||
|
// visualizer.js) to support editing values in place.
|
||||||
|
evaluateForEffect: requestContract({
|
||||||
|
params: types.type({
|
||||||
|
expression: types.string,
|
||||||
|
}),
|
||||||
|
result: types.type({
|
||||||
|
success: types.boolean,
|
||||||
|
message: types.string,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,8 +114,8 @@ function unchecked<T>(): types.Type<T, any, unknown> {
|
|||||||
return new types.Type<T, T, unknown>(
|
return new types.Type<T, T, unknown>(
|
||||||
"unchecked",
|
"unchecked",
|
||||||
(u): u is T => true,
|
(u): u is T => true,
|
||||||
value => types.success(value as T),
|
(value) => types.success(value as T),
|
||||||
value => value
|
(value) => value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es6",
|
"target": "ES2017",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"lib": ["es6"],
|
"lib": ["ES2017"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*", "../js-data-extractors/src/Extractors/TypeScriptDataExtractors.ts"]
|
||||||
"src/**/*",
|
|
||||||
"../js-data-extractors/src/Extractors/TypeScriptDataExtractors.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as webpack from "webpack";
|
|
||||||
import path = require("path");
|
|
||||||
import { CleanWebpackPlugin } from "clean-webpack-plugin";
|
import { CleanWebpackPlugin } from "clean-webpack-plugin";
|
||||||
import CopyPlugin = require("copy-webpack-plugin");
|
import * as CopyPlugin from "copy-webpack-plugin";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as webpack from "webpack";
|
||||||
|
|
||||||
const r = (file: string) => path.resolve(__dirname, file);
|
const r = (file: string) => path.resolve(__dirname, file);
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ module.exports = {
|
|||||||
"@hediet/debug-visualizer-data-extraction":
|
"@hediet/debug-visualizer-data-extraction":
|
||||||
"@hediet/debug-visualizer-data-extraction",
|
"@hediet/debug-visualizer-data-extraction",
|
||||||
"debug-visualizer-webview": "debug-visualizer-webview",
|
"debug-visualizer-webview": "debug-visualizer-webview",
|
||||||
fsevents: "require('fsevents')"
|
fsevents: "require('fsevents')",
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js"],
|
extensions: [".ts", ".js"],
|
||||||
@@ -55,11 +55,15 @@ function includeDependency(location: string) {
|
|||||||
});
|
});
|
||||||
const pkgName = JSON.parse(content).name;
|
const pkgName = JSON.parse(content).name;
|
||||||
|
|
||||||
return new CopyPlugin([
|
return new CopyPlugin({
|
||||||
{
|
patterns: [
|
||||||
from: location,
|
{
|
||||||
to: r(`./dist/node_modules/${pkgName}`),
|
from: location,
|
||||||
ignore: ["**/node_modules/**/*"],
|
to: r(`./dist/node_modules/${pkgName}`),
|
||||||
},
|
globOptions: {
|
||||||
]);
|
ignore: ["**/node_modules/**/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
"build": "yarn build-data-extraction && yarn build-ui && yarn build-extension",
|
"build": "yarn build-data-extraction && yarn build-ui && yarn build-extension",
|
||||||
"build-data-extraction": "yarn workspace @hediet/debug-visualizer-data-extraction build",
|
"build-data-extraction": "yarn workspace @hediet/debug-visualizer-data-extraction build",
|
||||||
"build-ui": "yarn workspace debug-visualizer-webview build",
|
"build-ui": "yarn workspace debug-visualizer-webview build",
|
||||||
"build-extension": "yarn workspace debug-visualizer build"
|
"build-extension": "yarn workspace debug-visualizer build",
|
||||||
|
"package": "yarn build && yarn workspace debug-visualizer package && node print-vsix-path.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^1.19.1"
|
"prettier": "^2.8.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
print-vsix-path.js
Normal file
13
print-vsix-path.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Run after `yarn workspace debug-visualizer package` -- vsce's own stdout
|
||||||
|
// ("DONE Packaged: ...") isn't meant to be parsed programmatically, so this
|
||||||
|
// just checks the fixed -o path that extension/package.json's own "package"
|
||||||
|
// script passes to vsce, and prints it (or fails loudly if it's missing).
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const vsixPath = path.resolve(__dirname, "extension", "dist", "extension.vsix");
|
||||||
|
if (!fs.existsSync(vsixPath)) {
|
||||||
|
console.error("Expected .vsix not found at " + vsixPath);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(vsixPath);
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack-dev-server --hot",
|
"dev": "webpack-dev-server --hot",
|
||||||
"build": "node --max_old_space_size=4096 ../node_modules/webpack/bin/webpack.js --mode production"
|
"build": "node --max_old_space_size=4096 ../node_modules/webpack/bin/webpack.js --mode development"
|
||||||
},
|
},
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -14,39 +14,38 @@
|
|||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"mobx": "^5.15.4",
|
"mobx": "^5.15.4",
|
||||||
"mobx-react": "^6.1.8",
|
"mobx-react": "^6.1.8",
|
||||||
"monaco-editor": "^0.20.0",
|
"monaco-editor": "^0.25.2",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-measure": "^2.3.0",
|
"react-measure": "^2.3.0",
|
||||||
"@hediet/debug-visualizer-data-extraction": "*",
|
"@hediet/debug-visualizer-data-extraction": "*",
|
||||||
"@hediet/visualization-bundle": "^0.2.1",
|
"@hediet/visualization-bundle": "^0.2.5",
|
||||||
"@hediet/visualization-core": "^0.2.0",
|
"@hediet/visualization-core": "^0.3.1",
|
||||||
"@knuddels/mobx-logger": "^1.1.1",
|
"@knuddels/mobx-logger": "^1.1.1",
|
||||||
"less-loader": "^6.2.0"
|
"less-loader": "^6.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/html-webpack-plugin": "^3.2.2",
|
"@types/html-webpack-plugin": "^3.2.2",
|
||||||
"@types/react-measure": "^2.0.6",
|
"@types/react-measure": "^2.0.6",
|
||||||
"@types/webpack": "^4.41.6",
|
|
||||||
"@types/classnames": "^2.2.9",
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/react": "^16.9.22",
|
"@types/react": "^16.9.22",
|
||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "^16.9.5",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^6.8.1",
|
||||||
"file-loader": "^5.1.0",
|
"file-loader": "^6.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^4.0.4",
|
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"monaco-editor-webpack-plugin": "^1.9.0",
|
"monaco-editor-webpack-plugin": "^4.0.0",
|
||||||
"sass": "^1.25.0",
|
"sass": "^1.25.0",
|
||||||
"raw-loader": "^4.0.0",
|
"raw-loader": "^4.0.0",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"style-loader": "^1.1.3",
|
"style-loader": "^1.1.3",
|
||||||
"ts-loader": "^6.2.1",
|
"ts-loader": "^9.4.4",
|
||||||
"ts-node": "^8.6.2",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^3.8.2",
|
"typescript": "^5.1.6",
|
||||||
"webpack": "^4.41.6",
|
"webpack": "^5.88.1",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^3.10.3"
|
"webpack-dev-server": "^4.15.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,18 @@ import * as monaco from "monaco-editor";
|
|||||||
export class ExpressionInput extends React.Component<{ model: Model }> {
|
export class ExpressionInput extends React.Component<{ model: Model }> {
|
||||||
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||||
@observable private contentHeight: number | undefined = undefined;
|
@observable private contentHeight: number | undefined = undefined;
|
||||||
private model = monaco.editor.createModel(
|
|
||||||
"",
|
private _model: monaco.editor.ITextModel | undefined = undefined;
|
||||||
"javascript",
|
private get model() {
|
||||||
monaco.Uri.parse(`file:///main.ts`)
|
if (!this._model) {
|
||||||
);
|
this._model = monaco.editor.createModel(
|
||||||
|
"",
|
||||||
|
"javascript",
|
||||||
|
monaco.Uri.parse(`file:///main.ts`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +90,7 @@ export class ExpressionInput extends React.Component<{ model: Model }> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.onDidContentSizeChange(e => {
|
this.editor.onDidContentSizeChange((e) => {
|
||||||
this.contentHeight = e.contentHeight;
|
this.contentHeight = e.contentHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +98,7 @@ export class ExpressionInput extends React.Component<{ model: Model }> {
|
|||||||
this.submit();
|
this.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.onKeyDown(e => {
|
this.editor.onKeyDown((e) => {
|
||||||
if (e.keyCode == monaco.KeyCode.Enter) {
|
if (e.keyCode == monaco.KeyCode.Enter) {
|
||||||
if (
|
if (
|
||||||
(this.model.getLineCount() <= 1 || e.ctrlKey) &&
|
(this.model.getLineCount() <= 1 || e.ctrlKey) &&
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Button, Spinner } from "@blueprintjs/core";
|
import { Button, Spinner } from "@blueprintjs/core";
|
||||||
import { observable, action } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import * as React from "react";
|
||||||
import { Model } from "../model/Model";
|
import { Model } from "../model/Model";
|
||||||
import { ExpressionInput } from "./ExpressionInput";
|
import { ExpressionInput } from "./ExpressionInput";
|
||||||
import { Visualizer } from "./Visualizer";
|
import { Visualizer } from "./Visualizer";
|
||||||
import classnames = require("classnames");
|
|
||||||
import { VisualizerHeaderDetails } from "./VisualizerHeaderDetails";
|
import { VisualizerHeaderDetails } from "./VisualizerHeaderDetails";
|
||||||
|
import classnames = require("classnames");
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class GUI extends React.Component<{ model: Model }> {
|
export class GUI extends React.Component<{ model: Model }> {
|
||||||
@@ -25,10 +25,7 @@ export class GUI extends React.Component<{ model: Model }> {
|
|||||||
<div className="part-Header">
|
<div className="part-Header">
|
||||||
<VisualizerHeader model={m} />
|
<VisualizerHeader model={m} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="part-Visualizer" style={{ flex: 1, minHeight: 0 }}>
|
||||||
className="part-Visualizer"
|
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
|
||||||
>
|
|
||||||
<Visualizer model={m} />
|
<Visualizer model={m} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,15 +40,9 @@ export class VisualizerHeader extends React.Component<{ model: Model }> {
|
|||||||
render() {
|
render() {
|
||||||
const m = this.props.model;
|
const m = this.props.model;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="component-VisualizerHeader" style={{ display: "flex", flexDirection: "row" }}>
|
||||||
className="component-VisualizerHeader"
|
|
||||||
style={{ display: "flex", flexDirection: "row" }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames("part-ExpandButton", this.expanded && "expanded")}
|
||||||
"part-ExpandButton",
|
|
||||||
this.expanded && "expanded"
|
|
||||||
)}
|
|
||||||
onClick={this.toggleExpanded}
|
onClick={this.toggleExpanded}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -87,6 +78,7 @@ export class VisualizerHeader extends React.Component<{ model: Model }> {
|
|||||||
@observer
|
@observer
|
||||||
export class VisualizerHeaderMain extends React.Component<{ model: Model }> {
|
export class VisualizerHeaderMain extends React.Component<{ model: Model }> {
|
||||||
render() {
|
render() {
|
||||||
|
const Spinner_ = Spinner as any; // WTF typescript??
|
||||||
const m = this.props.model;
|
const m = this.props.model;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -96,34 +88,19 @@ export class VisualizerHeaderMain extends React.Component<{ model: Model }> {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="part-ExpressionInput " style={{ flex: 1, minWidth: 0 }}>
|
||||||
className="part-ExpressionInput "
|
|
||||||
style={{ flex: 1, minWidth: 0 }}
|
|
||||||
>
|
|
||||||
<ExpressionInput model={m} />
|
<ExpressionInput model={m} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: 4 }} />
|
<div style={{ width: 4 }} />
|
||||||
{!m.isPolling &&
|
{!m.isPolling &&
|
||||||
(m.loading ? (
|
(m.loading ? (
|
||||||
<div style={{ padding: "0 4px" }}>
|
<div style={{ padding: "0 4px" }}>
|
||||||
<Spinner size={Spinner.SIZE_SMALL} />
|
<Spinner_ size={Spinner.SIZE_SMALL} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button minimal small className="part-Icon" icon="refresh" onClick={() => m.refresh()} />
|
||||||
minimal
|
|
||||||
small
|
|
||||||
className="part-Icon"
|
|
||||||
icon="refresh"
|
|
||||||
onClick={() => m.refresh()}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button minimal small className="part-Icon" icon="log-in" onClick={() => m.openBrowser()} />
|
||||||
minimal
|
|
||||||
small
|
|
||||||
className="part-Icon"
|
|
||||||
icon="log-in"
|
|
||||||
onClick={() => m.openBrowser()}
|
|
||||||
/>
|
|
||||||
{/*
|
{/*
|
||||||
TODO
|
TODO
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ export function hotComponent(
|
|||||||
|
|
||||||
return observer((props: any) => {
|
return observer((props: any) => {
|
||||||
const C = result!.component;
|
const C = result!.component;
|
||||||
return <C {...props} />;
|
const C_ = C as any; // WTF typescript/yarn??
|
||||||
|
return <C_ {...props} />;
|
||||||
}) as any;
|
}) as any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import * as monaco from "monaco-editor";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import "./style.scss";
|
|
||||||
import { App } from "./components/App";
|
import { App } from "./components/App";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
(globalThis as any).monaco = monaco;
|
||||||
|
|
||||||
const elem = document.createElement("div");
|
const elem = document.createElement("div");
|
||||||
elem.className = "react-root";
|
elem.className = "react-root";
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import {
|
import { DataExtractorId, VisualizationData } from "@hediet/debug-visualizer-data-extraction";
|
||||||
webviewContract,
|
|
||||||
DataExtractionState,
|
|
||||||
WindowWithWebviewData,
|
|
||||||
WebviewUrlParams,
|
|
||||||
} from "debug-visualizer/src/webviewContract";
|
|
||||||
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
|
||||||
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
|
|
||||||
import { observable, action, computed, when, runInAction } from "mobx";
|
|
||||||
import {
|
|
||||||
DataExtractorId,
|
|
||||||
VisualizationData,
|
|
||||||
} from "@hediet/debug-visualizer-data-extraction";
|
|
||||||
import "@hediet/visualization-bundle";
|
|
||||||
import {
|
|
||||||
globalVisualizationFactory,
|
|
||||||
VisualizationId,
|
|
||||||
Visualization,
|
|
||||||
} from "@hediet/visualization-core";
|
|
||||||
import { getApi as getVsCodeApi } from "./VsCodeApi";
|
|
||||||
import { MonacoBridge } from "./MonacoBridge";
|
|
||||||
import { Disposable } from "@hediet/std/disposable";
|
import { Disposable } from "@hediet/std/disposable";
|
||||||
import { startInterval, EventTimer } from "@hediet/std/timer";
|
import { EventTimer } from "@hediet/std/timer";
|
||||||
|
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
|
||||||
|
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
||||||
|
import "@hediet/visualization-bundle";
|
||||||
|
import "../visualizers/nodeTableGraphVisualizer";
|
||||||
|
import {
|
||||||
|
RegisterVisualizerFn,
|
||||||
|
Visualization,
|
||||||
|
VisualizationFactory,
|
||||||
|
VisualizationId,
|
||||||
|
createVisualizer,
|
||||||
|
globalVisualizationFactory,
|
||||||
|
libImplementation,
|
||||||
|
} from "@hediet/visualization-core";
|
||||||
|
import {
|
||||||
|
DataExtractionState,
|
||||||
|
WebviewUrlParams,
|
||||||
|
WindowWithWebviewData,
|
||||||
|
webviewContract,
|
||||||
|
} from "debug-visualizer/src/webviewContract";
|
||||||
|
import { ObservableMap, action, computed, observable, runInAction, when } from "mobx";
|
||||||
|
import { MonacoBridge } from "./MonacoBridge";
|
||||||
|
import { getApi as getVsCodeApi } from "./VsCodeApi";
|
||||||
|
|
||||||
declare const window: Window & WindowWithWebviewData;
|
declare const window: Window & WindowWithWebviewData;
|
||||||
|
|
||||||
@@ -28,8 +30,7 @@ declare let __webpack_public_path__: string;
|
|||||||
|
|
||||||
export class Model {
|
export class Model {
|
||||||
public readonly dispose = Disposable.fn();
|
public readonly dispose = Disposable.fn();
|
||||||
public readonly runningMode: "standalone" | "webview" | "webviewIFrame" =
|
public readonly runningMode: "standalone" | "webview" | "webviewIFrame" = "webview";
|
||||||
"webview";
|
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
public theme: "dark" | "light" = "light";
|
public theme: "dark" | "light" = "light";
|
||||||
@@ -47,14 +48,10 @@ export class Model {
|
|||||||
|
|
||||||
@observable languageId: string = "text";
|
@observable languageId: string = "text";
|
||||||
|
|
||||||
@observable private preferredVisualizationId:
|
@observable private preferredVisualizationId: VisualizationId | undefined = undefined;
|
||||||
| VisualizationId
|
|
||||||
| undefined = undefined;
|
|
||||||
|
|
||||||
@observable isPolling = false;
|
@observable isPolling = false;
|
||||||
private readonly pollingTimer = this.dispose.track(
|
private readonly pollingTimer = this.dispose.track(new EventTimer(500, "stopped"));
|
||||||
new EventTimer(500, "stopped")
|
|
||||||
);
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
public setPreferredVisualizationId(id: VisualizationId) {
|
public setPreferredVisualizationId(id: VisualizationId) {
|
||||||
@@ -83,10 +80,9 @@ export class Model {
|
|||||||
}
|
}
|
||||||
| undefined {
|
| undefined {
|
||||||
if (this.state.kind === "data") {
|
if (this.state.kind === "data") {
|
||||||
const vis = globalVisualizationFactory.getVisualizations(
|
const vis = visualizerRegistry
|
||||||
this.state.result.data,
|
.get()
|
||||||
this.preferredVisualizationId
|
.getVisualizations(this.state.result.data, this.preferredVisualizationId);
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
visualization: vis.bestVisualization,
|
visualization: vis.bestVisualization,
|
||||||
allVisualizations: vis.allVisualizations,
|
allVisualizations: vis.allVisualizations,
|
||||||
@@ -97,9 +93,7 @@ export class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observable.ref
|
@observable.ref
|
||||||
public server:
|
public server: typeof webviewContract.TServerInterface | undefined = undefined;
|
||||||
| typeof webviewContract.TServerInterface
|
|
||||||
| undefined = undefined;
|
|
||||||
|
|
||||||
private readonly vsCodeApi = getVsCodeApi<{ expression: string }>();
|
private readonly vsCodeApi = getVsCodeApi<{ expression: string }>();
|
||||||
|
|
||||||
@@ -126,9 +120,7 @@ export class Model {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const urlParams = (Object.fromEntries(
|
const urlParams = Object.fromEntries(url.searchParams.entries()) as unknown as WebviewUrlParams;
|
||||||
url.searchParams.entries()
|
|
||||||
) as unknown) as WebviewUrlParams;
|
|
||||||
const portStr = urlParams.serverPort;
|
const portStr = urlParams.serverPort;
|
||||||
if (!portStr) {
|
if (!portStr) {
|
||||||
throw new Error("No port given.");
|
throw new Error("No port given.");
|
||||||
@@ -161,7 +153,7 @@ export class Model {
|
|||||||
|
|
||||||
this.stayConnected();
|
this.stayConnected();
|
||||||
|
|
||||||
this.vsCodeApi.getState().then(state => {
|
this.vsCodeApi.getState().then((state) => {
|
||||||
if (state) {
|
if (state) {
|
||||||
this.setExpression(state.expression);
|
this.setExpression(state.expression);
|
||||||
}
|
}
|
||||||
@@ -170,6 +162,24 @@ export class Model {
|
|||||||
this.pollingTimer.onTick.sub(() => {
|
this.pollingTimer.onTick.sub(() => {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Exposed globally (not passed into custom visualizer scripts'
|
||||||
|
// closures) for the same reason setVisualizationModule is below:
|
||||||
|
// setCustomVisualizerScript's `eval()` runs plugin code in this
|
||||||
|
// same window/global scope, so a plugin (see vis-plugins/node-
|
||||||
|
// table-visualizer.js) can reach these to support editing values
|
||||||
|
// in place, without threading a new parameter through the whole
|
||||||
|
// register()/getVisualization()/render() plugin contract.
|
||||||
|
(globalThis as any).debugVisualizerSetValue = (expression: string) =>
|
||||||
|
this.evaluateForEffect(expression);
|
||||||
|
(globalThis as any).debugVisualizerRefresh = () => this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluateForEffect(expression: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
if (!this.server) {
|
||||||
|
return { success: false, message: "Not connected to the debug visualizer server." };
|
||||||
|
}
|
||||||
|
return this.server.evaluateForEffect({ expression });
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -209,29 +219,40 @@ export class Model {
|
|||||||
host: "localhost",
|
host: "localhost",
|
||||||
port: this.port,
|
port: this.port,
|
||||||
});
|
});
|
||||||
const { server } = webviewContract.getServerFromStream(
|
const { server } = webviewContract.getServerFromStream(stream, new ConsoleRpcLogger(), {
|
||||||
stream,
|
updateState: async ({ newState }) => {
|
||||||
new ConsoleRpcLogger(),
|
runInAction(() => {
|
||||||
{
|
this._loading = newState.kind === "loading";
|
||||||
updateState: async ({ newState }) => {
|
if (!this._loading) {
|
||||||
runInAction(() => {
|
this.state = newState;
|
||||||
this._loading = newState.kind === "loading";
|
}
|
||||||
if (!this._loading) {
|
});
|
||||||
this.state = newState;
|
},
|
||||||
}
|
setExpression: async ({ expression }) => {
|
||||||
});
|
this.setExpression(expression);
|
||||||
},
|
},
|
||||||
setExpression: async ({ expression }) => {
|
updateLanguageId: async ({ languageId }) => {
|
||||||
this.setExpression(expression);
|
this.languageId = languageId || "text";
|
||||||
},
|
},
|
||||||
updateLanguageId: async ({ languageId }) => {
|
setTheme: async ({ theme }) => {
|
||||||
this.languageId = languageId || "text";
|
this.theme = theme;
|
||||||
},
|
},
|
||||||
setTheme: async ({ theme }) => {
|
setCustomVisualizerScript: async ({ id, jsSource }) => {
|
||||||
this.theme = theme;
|
eval(`
|
||||||
},
|
((load) => {
|
||||||
}
|
let fn = undefined;
|
||||||
);
|
if (load) {
|
||||||
|
const module = {};
|
||||||
|
load(module);
|
||||||
|
fn = module.exports;
|
||||||
|
}
|
||||||
|
setVisualizationModule(${JSON.stringify(id)}, fn);
|
||||||
|
})(
|
||||||
|
${jsSource ? `function (module) { ${jsSource} }` : "undefined"}
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await server.authenticate({ secret: this.serverSecret });
|
await server.authenticate({ secret: this.serverSecret });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -254,3 +275,28 @@ export class Model {
|
|||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fns = new ObservableMap<string, RegisterVisualizerFn>();
|
||||||
|
const visualizerRegistry = computed(() => {
|
||||||
|
const result = new VisualizationFactory();
|
||||||
|
for (const visualization of globalVisualizationFactory.getRegisteredVisualizers()) {
|
||||||
|
result.addVisualizer(visualization);
|
||||||
|
}
|
||||||
|
for (const visualization of globalVisualizationFactory.getRegisteredHiddenVisualizer()) {
|
||||||
|
result.addHiddenVisualizer(visualization);
|
||||||
|
}
|
||||||
|
fns.forEach((fn) => {
|
||||||
|
fn((options) => result.addVisualizer(createVisualizer(options)), libImplementation);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
function setVisualizationModule(id: string, moduleFn: RegisterVisualizerFn | undefined) {
|
||||||
|
if (moduleFn) {
|
||||||
|
fns.set(id, moduleFn);
|
||||||
|
} else {
|
||||||
|
fns.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(globalThis as any).setVisualizationModule = setVisualizationModule;
|
||||||
|
|||||||
1764
webview/src/visualizers/nodeTableGraphVisualizer.ts
Normal file
1764
webview/src/visualizers/nodeTableGraphVisualizer.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "ES2017",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true,
|
"lib": ["DOM", "ES2023", "DOM.Iterable"],
|
||||||
"rootDir": "./src",
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"rootDir": "./src",
|
||||||
"newLine": "LF",
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"newLine": "LF",
|
||||||
"jsx": "react",
|
"sourceMap": true,
|
||||||
"experimentalDecorators": true
|
"jsx": "react",
|
||||||
},
|
"experimentalDecorators": true,
|
||||||
"include": ["src/**/*"]
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as webpack from "webpack";
|
|
||||||
import path = require("path");
|
|
||||||
import HtmlWebpackPlugin = require("html-webpack-plugin");
|
|
||||||
import MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
|
|
||||||
import ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
|
|
||||||
import { CleanWebpackPlugin } from "clean-webpack-plugin";
|
import { CleanWebpackPlugin } from "clean-webpack-plugin";
|
||||||
|
import * as HtmlWebpackPlugin from "html-webpack-plugin";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as webpack from "webpack";
|
||||||
|
import ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
|
||||||
|
import MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
|
||||||
|
|
||||||
const r = (file: string) => path.resolve(__dirname, file);
|
const r = (file: string) => path.resolve(__dirname, file);
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ module.exports = {
|
|||||||
path: r("dist"),
|
path: r("dist"),
|
||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
chunkFilename: "[name]-[hash].js",
|
chunkFilename: "[name]-[hash].js",
|
||||||
devtoolModuleFilenameTemplate: info => {
|
devtoolModuleFilenameTemplate: (info: any) => {
|
||||||
let result = info.absoluteResourcePath.replace(/\\/g, "/");
|
let result = info.absoluteResourcePath.replace(/\\/g, "/");
|
||||||
if (!result.startsWith("file:")) {
|
if (!result.startsWith("file:")) {
|
||||||
// Some paths already start with the file scheme.
|
// Some paths already start with the file scheme.
|
||||||
@@ -30,10 +30,10 @@ module.exports = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.less$/,
|
test: /\.less$/,
|
||||||
loaders: ["style-loader", "css-loader", "less-loader"],
|
use: ["style-loader", "css-loader", "less-loader"],
|
||||||
},
|
},
|
||||||
{ test: /\.css$/, loader: "style-loader!css-loader" },
|
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
|
||||||
{ test: /\.scss$/, loader: "style-loader!css-loader!sass-loader" },
|
{ test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] },
|
||||||
{
|
{
|
||||||
test: /\.(jpe?g|png|gif|eot|ttf|svg|woff|woff2|md)$/i,
|
test: /\.(jpe?g|png|gif|eot|ttf|svg|woff|woff2|md)$/i,
|
||||||
loader: "file-loader",
|
loader: "file-loader",
|
||||||
@@ -45,9 +45,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
node: {
|
/*node: {
|
||||||
fs: "empty",
|
fs: "empty",
|
||||||
},
|
},*/
|
||||||
plugins: (() => {
|
plugins: (() => {
|
||||||
const plugins: any[] = [
|
const plugins: any[] = [
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -55,10 +55,13 @@ module.exports = {
|
|||||||
}),
|
}),
|
||||||
new ForkTsCheckerWebpackPlugin(),
|
new ForkTsCheckerWebpackPlugin(),
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(),
|
||||||
new MonacoWebpackPlugin({
|
new webpack.DefinePlugin({
|
||||||
|
"process.env": JSON.stringify(process.env),
|
||||||
|
}),
|
||||||
|
/*new MonacoWebpackPlugin({
|
||||||
// Add more languages here once webworker issues are solved.
|
// Add more languages here once webworker issues are solved.
|
||||||
languages: ["typescript"],
|
languages: ["typescript"],
|
||||||
}),
|
}),*/
|
||||||
];
|
];
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
|
|||||||
Reference in New Issue
Block a user