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

@@ -27,7 +27,30 @@ export class Config {
);
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);
return tpl.render({
workspaceFolder: () => {

View File

@@ -1,7 +1,7 @@
import {
DataExtractionResult,
DataResult,
getExpressionForDataExtractorApi,
getExpressionForDataExtractorApi,
getExpressionToInitializeDataExtractorApi,
ApiHasNotBeenInitializedCode,
} from "@hediet/debug-visualizer-data-extraction";
@@ -20,6 +20,8 @@ import {
VisualizationBackendBase,
} from "./VisualizationBackend";
import { Disposable } from "@hediet/std/disposable";
import { DebouncedRunner } from "../utils/DebouncedRunner";
import { FileWatcher } from "../webview/WebviewConnection";
registerUpdateReconciler(module);
@@ -232,199 +234,76 @@ class JsVisualizationBackend extends VisualizationBackendBase {
private async initializeCustomScripts(): Promise<void> {
this.dispose.untrack(this.customScripts);
this.customScripts?.dispose();
this.customScripts = this.dispose.track(new CustomScripts(this.debugSession, this.config, () => this.onChangeEmitter.emit()));
this.customScripts = this.dispose.track(
new CustomScripts(this.debugSession, this.config, () =>
this.onChangeEmitter.emit()
)
);
}
}
class CustomScripts {
public readonly dispose = Disposable.fn();
@observable
private filesVersionId: number = 0;
private readonly fileChangeDebouncedScheduler = new DebouncedScheduler(100);
constructor(private readonly debugSession: DebugSessionProxy, private readonly config: Config, private readonly changeHandler: () => void) {
this.init();
}
private init(): void {
const watchers = new Map<string, WatchedFile>();
this.dispose.track({
dispose: () => {
for (const w of watchers.values()) {
w.dispose();
}
},
});
this.dispose.track({
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,
() => {
this.fileChangeDebouncedScheduler.run(
() => {
this.filesVersionId++;
}
);
}
);
watchers.set(fsPath, entry);
}
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.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.changeHandler();
}
},
{
fireImmediately: true,
}
),
});
}
}
interface Result {
error?: { message: string };
}
class WatchedFile {
public static nextId = 0;
private readonly watcher = watch(
this.fsPath,
{ encoding: "utf-8" },
this.changeHandler
);
public readonly id = WatchedFile.nextId++;
public lastContent: string | undefined = undefined;
constructor(
public readonly fsPath: string,
private readonly debugSession: DebugSessionProxy,
private readonly changeHandler: () => void
) {}
debugSession: DebugSessionProxy,
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 dispose(): void {
this.watcher.close();
}
let expression = `
(
runCode => {
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} }`
}
)`;
public async update(): Promise<Result> {
if (!existsSync(this.fsPath)) {
return {
error: { message: `File does not exist.` },
};
}
try {
await debugSession.evaluate({
expression,
frameId: undefined,
context: "repl",
});
} catch (e) {
window.showErrorMessage(
'Error while running custom visualization extractor script "' +
file.path +
'": ' +
e.message
);
}
}
const scriptContent = readFileSync(this.fsPath, {
encoding: "utf-8",
});
if (this.lastContent === scriptContent) {
return {};
}
this.lastContent = scriptContent;
const expression = `
(
load => {
var module = {};
load(module);
${getExpressionForDataExtractorApi()}.registerDataExtractorsSource("${
this.id
}", module.exports);
changeHandler();
}
)
(
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 = `${getExpressionForDataExtractorApi()}.unregisterDataExtractorsSource("${
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();
);
}
}

View 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();
}
}

View 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();
}

View File

@@ -11,9 +11,15 @@ import { WebviewServer } from "./WebviewServer";
import * as open from "open";
import chromeLauncher = require("chrome-launcher");
import { Config } from "../Config";
import { IncrementalMap } from "../utils/IncrementalMap";
import { watch, existsSync, readFileSync } from "fs";
import { DebouncedRunner } from "../utils/DebouncedRunner";
import { getExpressionForDataExtractorApi } from "@hediet/debug-visualizer-data-extraction";
import { window } from "vscode";
export class WebviewConnection {
public readonly dispose = Disposable.fn();
@observable
private watcher: VisualizationWatch | undefined = undefined;
@@ -57,8 +63,8 @@ export class WebviewConnection {
let oldPreferredDataExtractor: VisualizationWatch["preferredDataExtractor"];
if (this.watcher) {
oldPreferredDataExtractor = this.watcher
.preferredDataExtractor;
oldPreferredDataExtractor =
this.watcher.preferredDataExtractor;
this.dispose.untrack(this.watcher).dispose();
}
this.watcher = this.dispose.track(
@@ -95,10 +101,11 @@ export class WebviewConnection {
getCompletions: async ({ text, column }) => {
throwIfNotAuthenticated();
const completions = await evaluationWatchService.getCompletions(
text,
column
);
const completions =
await evaluationWatchService.getCompletions(
text,
column
);
return {
completions,
};
@@ -108,6 +115,36 @@ export class WebviewConnection {
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) {
window.showErrorMessage(
'Error while running custom visualization extractor script "' +
file.path +
'": ' +
e.message
);
}
}
}
)
);
this.dispose.track([
Disposable.create(
autorun(() => {
@@ -153,3 +190,95 @@ async function launchChrome(url: string): Promise<boolean> {
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.scheduleRefresh(this);
}
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;
}
}

View File

@@ -54,6 +54,13 @@ export const webviewContract = contract({
]),
}),
}),
setCustomVisualizerScript: notificationContract({
params: types.type({
id: types.string,
jsSource: types.union([types.string, types.null]),
}),
}),
},
server: {
authenticate: requestContract({
@@ -90,8 +97,8 @@ function unchecked<T>(): types.Type<T, any, unknown> {
return new types.Type<T, T, unknown>(
"unchecked",
(u): u is T => true,
value => types.success(value as T),
value => value
(value) => types.success(value as T),
(value) => value
);
}