This commit is contained in:
Henning Dieterichs
2022-12-01 16:14:17 +01:00
parent 1b6fabfe29
commit 904d0060ea
24 changed files with 1841 additions and 580 deletions

View File

@@ -1,96 +1,95 @@
import {
VisualizationData,
DataExtractionResult,
} from "../../DataExtractionResult";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
export interface DataExtractorApi {
/**
* Registers a single extractor.
*/
registerExtractor(extractor: DataExtractor): void;
/**
* Registers multiple extractors.
*/
registerExtractors(extractors: DataExtractor[]): void;
/**
* Extracts data from the result of `valueFn`.
* @valueFn a function returning the value to extract the data from.
* Is a function so that it's evaluation can depend on `evalFn`.
*/
getData(
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, unknown>
): JSONString<DataResult>;
/**
* Registers all default (built-in) extractors.
* @preferExisting if `true`, existing extractors with the same id are not overwritten.
*/
registerDefaultExtractors(preferExisting?: boolean): void;
registerDataExtractorsSource(id: string, fn: LoadDataExtractorsFn): void;
unregisterDataExtractorsSource(id: string): void;
}
export type DataResult =
| {
kind: "Data";
extractionResult: DataExtractionResult;
}
| { kind: "NoExtractors" }
| { kind: "Error"; message: string };
export interface JSONString<T> extends String {
__brand: { json: T };
}
export interface DataExtractor {
/**
* Must be unique among all data extractors.
*/
id: string;
getExtractions(
data: unknown,
extractionCollector: ExtractionCollector,
context: DataExtractorContext
): void;
}
export interface ExtractionCollector {
/**
* Suggests a possible extraction.
*/
addExtraction(extraction: DataExtraction): void;
}
export interface DataExtractorContext {
/**
* Evaluates an expression in the context of the active stack frame.
*/
evalFn: <TEval>(expression: string) => TEval;
variablesInScope: Record<string, () => unknown>;
}
export interface DataExtraction {
/**
* Higher priorities are preferred.
*/
priority: number;
/**
* A unique id identifying this extraction among all extractions.
* Required to express extraction preferences.
*/
id: string;
/**
* A user friendly name of this extraction.
*/
name: string;
extractData(): VisualizationData;
}
import {
VisualizationData,
DataExtractionResult,
} from "../../DataExtractionResult";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
export interface DataExtractorApi {
/**
* Registers a single extractor.
*/
registerExtractor(extractor: DataExtractor): void;
/**
* Registers multiple extractors.
*/
registerExtractors(extractors: DataExtractor[]): void;
/**
* Extracts data from the result of `valueFn`.
* @valueFn a function returning the value to extract the data from.
* Is a function so that it's evaluation can depend on `evalFn`.
*/
getData(
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, unknown>
): JSONString<DataResult>;
/**
* Registers all default (built-in) extractors.
* @preferExisting if `true`, existing extractors with the same id are not overwritten.
*/
registerDefaultExtractors(preferExisting?: boolean): void;
setDataExtractorFn(id: string, fn: LoadDataExtractorsFn | undefined): void;
}
export type DataResult =
| {
kind: "Data";
extractionResult: DataExtractionResult;
}
| { kind: "NoExtractors" }
| { kind: "Error"; message: string };
export interface JSONString<T> extends String {
__brand: { json: T };
}
export interface DataExtractor {
/**
* Must be unique among all data extractors.
*/
id: string;
getExtractions(
data: unknown,
extractionCollector: ExtractionCollector,
context: DataExtractorContext
): void;
}
export interface ExtractionCollector {
/**
* Suggests a possible extraction.
*/
addExtraction(extraction: DataExtraction): void;
}
export interface DataExtractorContext {
/**
* Evaluates an expression in the context of the active stack frame.
*/
evalFn: <TEval>(expression: string) => TEval;
variablesInScope: Record<string, () => unknown>;
}
export interface DataExtraction {
/**
* Higher priorities are preferred.
*/
priority: number;
/**
* A unique id identifying this extraction among all extractions.
* Required to express extraction preferences.
*/
id: string;
/**
* A user friendly name of this extraction.
*/
name: string;
extractData(): VisualizationData;
}

View File

@@ -1,122 +1,122 @@
import {
DataExtractorApi,
DataResult,
JSONString,
DataExtractor,
DataExtraction,
ExtractionCollector,
DataExtractorContext,
} from "./DataExtractorApi";
import { DataExtractorInfo } from "../../DataExtractionResult";
import { registerDefaultExtractors } from "./default-extractors";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
import * as helpers from "../helpers";
/**
* @internal
*/
export class DataExtractorApiImpl implements DataExtractorApi {
public static lastContext: DataExtractorContext | undefined = undefined;
private readonly extractors = new Map<string, DataExtractor>();
private readonly extractorSources = new Map<string, LoadDataExtractorsFn>();
private toJson<TData>(data: TData): JSONString<TData> {
return JSON.stringify(data) as any;
}
public registerExtractor(extractor: DataExtractor): void {
this.extractors.set(extractor.id, extractor);
}
public registerExtractors(extractors: DataExtractor[]): void {
for (const e of extractors) {
this.registerExtractor(e);
}
}
public getData(
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, () => unknown>
): JSONString<DataResult> {
const extractions = new Array<DataExtraction>();
const extractionCollector: ExtractionCollector = {
addExtraction(extraction) {
extractions.push(extraction);
},
};
const context: DataExtractorContext = {
evalFn,
variablesInScope,
};
DataExtractorApiImpl.lastContext = context;
const value = valueFn();
const extractors = new Array<DataExtractor>();
for (const fn of this.extractorSources.values()) {
fn((extractor) => {
extractors.push(extractor);
}, helpers);
}
for (const e of [...this.extractors.values(), ...extractors]) {
e.getExtractions(value, extractionCollector, context);
}
DataExtractorApiImpl.lastContext = undefined;
extractions.sort((a, b) => b.priority - a.priority);
let usedExtraction = extractions[0];
if (!usedExtraction) {
return this.toJson({ kind: "NoExtractors" } as DataResult);
}
if (preferredDataExtractorId) {
const preferred = extractions.find(
(e) => e.id === preferredDataExtractorId
);
if (preferred) {
usedExtraction = preferred;
}
}
function mapExtractor(e: DataExtraction): DataExtractorInfo {
return {
id: e.id as any,
name: e.name,
priority: e.priority,
};
}
const data = usedExtraction.extractData();
return this.toJson({
kind: "Data",
extractionResult: {
data,
usedExtractor: mapExtractor(usedExtraction),
availableExtractors: extractions.map(mapExtractor),
},
} as DataResult);
}
public registerDefaultExtractors(preferExisting: boolean = false): void {
// TODO consider preferExisting
registerDefaultExtractors(this);
}
public registerDataExtractorsSource(
id: string,
fn: LoadDataExtractorsFn
): void {
this.extractorSources.set(id, fn);
}
public unregisterDataExtractorsSource(id: string): void {
this.extractorSources.delete(id);
}
}
import {
DataExtractorApi,
DataResult,
JSONString,
DataExtractor,
DataExtraction,
ExtractionCollector,
DataExtractorContext,
} from "./DataExtractorApi";
import { DataExtractorInfo } from "../../DataExtractionResult";
import { registerDefaultExtractors } from "./default-extractors";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
import * as helpers from "../helpers";
/**
* @internal
*/
export class DataExtractorApiImpl implements DataExtractorApi {
public static lastContext: DataExtractorContext | undefined = undefined;
private readonly extractors = new Map<string, DataExtractor>();
private readonly extractorSources = new Map<string, LoadDataExtractorsFn>();
private toJson<TData>(data: TData): JSONString<TData> {
return JSON.stringify(data) as any;
}
public registerExtractor(extractor: DataExtractor): void {
this.extractors.set(extractor.id, extractor);
}
public registerExtractors(extractors: DataExtractor[]): void {
for (const e of extractors) {
this.registerExtractor(e);
}
}
public getData(
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, () => unknown>
): JSONString<DataResult> {
const extractions = new Array<DataExtraction>();
const extractionCollector: ExtractionCollector = {
addExtraction(extraction) {
extractions.push(extraction);
},
};
const context: DataExtractorContext = {
evalFn,
variablesInScope,
};
DataExtractorApiImpl.lastContext = context;
const value = valueFn();
const extractors = new Array<DataExtractor>();
for (const fn of this.extractorSources.values()) {
fn((extractor) => {
extractors.push(extractor);
}, helpers);
}
for (const e of [...this.extractors.values(), ...extractors]) {
e.getExtractions(value, extractionCollector, context);
}
DataExtractorApiImpl.lastContext = undefined;
extractions.sort((a, b) => b.priority - a.priority);
let usedExtraction = extractions[0];
if (!usedExtraction) {
return this.toJson({ kind: "NoExtractors" } as DataResult);
}
if (preferredDataExtractorId) {
const preferred = extractions.find(
(e) => e.id === preferredDataExtractorId
);
if (preferred) {
usedExtraction = preferred;
}
}
function mapExtractor(e: DataExtraction): DataExtractorInfo {
return {
id: e.id as any,
name: e.name,
priority: e.priority,
};
}
const data = usedExtraction.extractData();
return this.toJson({
kind: "Data",
extractionResult: {
data,
usedExtractor: mapExtractor(usedExtraction),
availableExtractors: extractions.map(mapExtractor),
},
} as DataResult);
}
public registerDefaultExtractors(preferExisting: boolean = false): void {
// TODO consider preferExisting
registerDefaultExtractors(this);
}
public setDataExtractorFn(
id: string,
fn: LoadDataExtractorsFn | undefined
): void {
if (fn) {
this.extractorSources.set(id, fn);
} else {
this.extractorSources.delete(id);
}
}
}

View File

@@ -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);
},
});
}
}

View File

@@ -4,7 +4,12 @@ import {
ExtractionCollector,
DataExtractorContext,
} from "../DataExtractorApi";
import { createGraph, CreateGraphEdge } from "../../helpers";
import {
createGraph,
CreateGraphEdge,
createGraphFromPointers,
} from "../../helpers";
import { GraphNode } from "../../..";
export class ObjectGraphExtractor implements DataExtractor {
readonly id = "object-graph";
@@ -27,53 +32,83 @@ export class ObjectGraphExtractor implements DataExtractor {
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({
id: "object-graph",
name: "Object Graph",
priority: 99,
priority: 98,
extractData() {
return createGraph(
[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 }
);
return createGraph([data], infoSelector, { 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,
});
},
});
}
}
}

View File

@@ -1,22 +1,33 @@
import { DataExtractor, DataExtractorContext, ExtractionCollector } from "../..";
export class StringDiffExtractor implements DataExtractor {
public readonly id = "string-diff";
public getExtractions(data: unknown,
collector: ExtractionCollector,
context: DataExtractorContext) {
if (Array.isArray(data) && data.length === 2 && typeof data[0] === "string" && typeof data[1] === "string") {
collector.addExtraction({
id: "string-diff",
name: "String Diff",
priority: 1000,
extractData: () => ({
kind: { text: true },
text: data[0],
otherText: data[1]
})
})
}
}
}
import {
DataExtractor,
DataExtractorContext,
ExtractionCollector,
} from "../..";
export class StringDiffExtractor implements DataExtractor {
public readonly id = "string-diff";
public getExtractions(
data: unknown,
collector: ExtractionCollector,
context: DataExtractorContext
) {
if (
Array.isArray(data) &&
data.length === 2 &&
typeof data[0] === "string" &&
typeof data[1] === "string"
) {
collector.addExtraction({
id: "string-diff",
name: "String Diff",
priority: 1300,
extractData: () => ({
kind: { text: true },
text: data[0],
otherText: data[1],
}),
});
}
}
}

View File

@@ -0,0 +1,112 @@
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") {
item = context.evalFn(item);
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,
});
},
});
}
}

View File

@@ -16,7 +16,9 @@ export class TableDataExtractor implements DataExtractor {
if (!Array.isArray(data)) {
return;
}
if (!data.every(d => typeof d === "object" && d)) {
if (
!data.every((d) => typeof d === "object" && d && !Array.isArray(d))
) {
return;
}
assert<object[]>(data);
@@ -44,7 +46,7 @@ export class TableDataExtractor implements DataExtractor {
kind: {
table: true,
},
rows: data.map(d => ({ type: d.constructor.name, ...d })),
rows: data.map((d) => ({ type: d.constructor.name, ...d })),
});
},
});

View File

@@ -9,6 +9,8 @@ import { getDataExtractorApi } from "../injection";
import { GridExtractor } from "./GridExtractor";
import { TableDataExtractor } from "./TableExtractor";
import { StringDiffExtractor } from "./StringDiffExtractor";
import { StringRangeExtractor } from "./StringRangeExtractor";
import { MarkedGridFromArrayExtractor } from "./MarkedGridExtractor";
/**
* The default data extractors should be registered by VS Code automatically.
@@ -27,6 +29,8 @@ export function registerDefaultExtractors(
new GridExtractor(),
new TableDataExtractor(),
new StringDiffExtractor(),
new StringRangeExtractor(),
new MarkedGridFromArrayExtractor(),
]) {
api.registerExtractor(item);
}