Code improvements.

This commit is contained in:
hediet
2020-08-14 16:10:45 +02:00
parent 7ba2428819
commit 24aa6857e5
25 changed files with 635 additions and 447 deletions

View File

@@ -7,7 +7,7 @@
"version": "2.0.0",
"license": "GPL-3.0",
"engines": {
"vscode": "^1.35.0"
"vscode": "^1.47.0"
},
"publisher": "hediet",
"keywords": [
@@ -108,7 +108,8 @@
"open": "^7.0.2",
"serve-static": "^1.14.1",
"ws": "^7.2.1",
"crypto-random-string": "^3.1.0"
"crypto-random-string": "^3.1.0",
"mobx-utils": "^5.6.1"
},
"devDependencies": {
"copy-webpack-plugin": "^5.1.1",
@@ -116,7 +117,7 @@
"@types/express": "^4.17.2",
"@types/serve-static": "^1.13.3",
"@types/node": "^13.7.4",
"@types/vscode": "1.35.0",
"@types/vscode": "1.47.0",
"tslint": "^6.0.0",
"typescript": "^3.8.2",
"webpack": "^4.41.6",

View File

@@ -1,55 +1,50 @@
import { workspace } from "vscode";
import { workspace, window, ColorThemeKind } from "vscode";
import { Disposable } from "@hediet/std/disposable";
import { observable } from "mobx";
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
const useChromeKioskModeKey = "debugVisualizer.useChromeKioskMode";
const debugAdapterConfigsKey = "debugVisualizer.debugAdapterConfigurations";
import { VsCodeSetting, serializerWithDefault } from "./utils/VsCodeSettings";
export class Config {
public dispose = Disposable.fn();
@observable
private _useChromeKioskMode!: boolean;
@observable
private _debugAdapterConfigs!: DebugAdapterConfigs;
private readonly _useChromeKioskMode = new VsCodeSetting(
"debugVisualizer.useChromeKioskMode",
{ serializer: serializerWithDefault<boolean>(true) }
);
public get useChromeKioskMode(): boolean {
return this._useChromeKioskMode;
return this._useChromeKioskMode.get();
}
public get debugAdapterConfigs(): DebugAdapterConfigs {
return this._debugAdapterConfigs;
private readonly _debugAdapterConfigs = new VsCodeSetting(
"debugVisualizer.debugAdapterConfigurations",
{ serializer: serializerWithDefault<DebugAdapterConfigs>({}) }
);
@observable
private _vsCodeTheme = window.activeColorTheme;
public get theme(): "light" | "dark" {
if (this._vsCodeTheme.kind === ColorThemeKind.Light) {
return "light";
} else if (this._vsCodeTheme.kind === ColorThemeKind.Dark) {
return "dark";
}
return "light";
}
constructor() {
this.updateConfig();
this.dispose.track(
workspace.onDidChangeConfiguration(() => {
this.updateConfig();
window.onDidChangeActiveColorTheme(() => {
this._vsCodeTheme = window.activeColorTheme;
})
);
}
private updateConfig(): void {
const c = workspace.getConfiguration();
this._useChromeKioskMode = mapUndefined(
c.get<boolean>(useChromeKioskModeKey),
true
);
this._debugAdapterConfigs = mapUndefined(
c.get<DebugAdapterConfigs>(debugAdapterConfigsKey),
{}
);
}
public getDebugAdapterConfig(
debugAdapterType: string
): DebugAdapterConfig | undefined {
const c = this.debugAdapterConfigs[debugAdapterType];
const c = this._debugAdapterConfigs.get()[debugAdapterType];
if (!c) {
return undefined;
}
@@ -65,13 +60,6 @@ export class Config {
}
}
function mapUndefined<T>(val: T | undefined, defaultVal: T) {
if (val === undefined) {
return defaultVal;
}
return val;
}
type DebugAdapterConfigs = {
[debugAdapter: string]: {
context?: "watch" | "repl";

View File

@@ -1,10 +1,10 @@
import { EvaluationEngine, Evaluator } from "./EvaluationEngine";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
export class ComposedEvaluationEngine implements EvaluationEngine {
constructor(public readonly engines: EvaluationEngine[]) {}
createEvaluator(session: VsCodeDebugSession): Evaluator | undefined {
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
for (const f of this.engines) {
const evaluator = f.createEvaluator(session);
if (evaluator) {

View File

@@ -1,5 +1,5 @@
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
import { EvaluationEngine, Evaluator } from "./EvaluationEngine";
import { Config, DebugAdapterConfig } from "../../Config";
import { GenericEvaluator } from "./GenericEvaluationEngine";
@@ -11,7 +11,7 @@ registerUpdateReconciler(module);
export class ConfiguredEvaluationEngine implements EvaluationEngine {
constructor(private readonly config: Config) {}
createEvaluator(session: VsCodeDebugSession): Evaluator | undefined {
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
const config = this.config.getDebugAdapterConfig(session.session.type);
if (!config) {
return undefined;
@@ -22,7 +22,7 @@ export class ConfiguredEvaluationEngine implements EvaluationEngine {
class ConfiguredEvaluator extends GenericEvaluator {
constructor(
session: VsCodeDebugSession,
session: EnhancedDebugSession,
private readonly config: DebugAdapterConfig
) {
super(session);

View File

@@ -2,11 +2,11 @@ import {
DataExtractionResult,
DataExtractorId,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import { FormattedMessage } from "../../contract";
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
import { FormattedMessage } from "../../webviewContract";
export interface EvaluationEngine {
createEvaluator(session: VsCodeDebugSession): Evaluator | undefined;
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined;
}
export interface Evaluator {

View File

@@ -2,21 +2,21 @@ import {
DataExtractionResult,
DataExtractorId,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
import {
EvaluationEngine,
Evaluator,
EvaluationArgs,
} from "./EvaluationEngine";
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
import { FormattedMessage } from "../../contract";
import { FormattedMessage } from "../../webviewContract";
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
registerUpdateReconciler(module);
@hotClass(module)
export class GenericEvaluationEngine implements EvaluationEngine {
createEvaluator(session: VsCodeDebugSession): Evaluator | undefined {
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
return new GenericEvaluator(session);
}
}
@@ -24,7 +24,7 @@ export class GenericEvaluationEngine implements EvaluationEngine {
export class GenericEvaluator implements Evaluator {
public readonly languageId = "text";
constructor(private readonly session: VsCodeDebugSession) {}
constructor(private readonly session: EnhancedDebugSession) {}
public async evaluate({
expression,

View File

@@ -5,20 +5,20 @@ import {
getExpressionToInitializeDataExtractorApi,
DataExtractionResult,
} from "@hediet/debug-visualizer-data-extraction";
import { VsCodeDebugSession } from "../../VsCodeDebugger";
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
import {
EvaluationEngine,
Evaluator,
EvaluationArgs,
} from "./EvaluationEngine";
import { FormattedMessage } from "../../contract";
import { FormattedMessage } from "../../webviewContract";
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
registerUpdateReconciler(module);
@hotClass(module)
export class JsEvaluationEngine implements EvaluationEngine {
createEvaluator(session: VsCodeDebugSession): Evaluator | undefined {
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
const supportedDebugAdapters = [
"node",
"node2",
@@ -37,7 +37,7 @@ export class JsEvaluationEngine implements EvaluationEngine {
class JsEvaluator implements Evaluator {
public readonly languageId = "javascript";
constructor(private readonly session: VsCodeDebugSession) {}
constructor(private readonly session: EnhancedDebugSession) {}
private getContext(): "copy" | "repl" {
if (this.session.session.type.startsWith("pwa-")) {

View File

@@ -2,7 +2,7 @@ import {
DataExtractionResult,
isExtractedData,
} from "@hediet/debug-visualizer-data-extraction";
import { FormattedMessage } from "../../contract";
import { FormattedMessage } from "../../webviewContract";
export interface ParseEvaluationResultContext {
debugAdapterType: string;

View File

@@ -1,5 +1,5 @@
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
import { DataExtractionState, CompletionItem } from "../contract";
import { DataExtractionState, CompletionItem } from "../webviewContract";
export interface EvaluationWatchService {
createEvaluationWatcher(

View File

@@ -6,9 +6,9 @@ import {
import { observable, autorun, action } from "mobx";
import { Disposable } from "@hediet/std/disposable";
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
import { DataExtractionState, CompletionItem } from "../contract";
import { DataExtractionState, CompletionItem } from "../webviewContract";
import { hotClass } from "@hediet/node-reload";
import { VsCodeDebuggerView } from "../VsCodeDebugger";
import { VsCodeDebuggerView } from "../debugger/VsCodeDebuggerView";
import { EvaluationEngine } from "./EvaluationEngine/EvaluationEngine";
@hotClass(module)

View File

@@ -1,214 +0,0 @@
import { Disposable } from "@hediet/std/disposable";
import { debug, DebugSession } from "vscode";
import { EventEmitter } from "@hediet/std/events";
import { CompletionItem } from "./contract";
import { observable, runInAction, action } from "mobx";
export class VsCodeDebugger {
public readonly dispose = Disposable.fn();
private readonly sessions = new Map<DebugSession, VsCodeDebugSession>();
private readonly _onDidStartDebugSession = new EventEmitter<{
session: VsCodeDebugSession;
}>();
public readonly onDidStartDebugSession = this._onDidStartDebugSession.asEvent();
public getDebugSession(session: DebugSession): VsCodeDebugSession {
let result = this.sessions.get(session);
if (!result) {
result = new VsCodeDebugSession(session);
this.sessions.set(session, result);
}
return result;
}
constructor() {
this.dispose.track([
debug.onDidStartDebugSession(session => {
const e = this.sessions.get(session)!;
this._onDidStartDebugSession.emit({ session: e });
}),
debug.onDidTerminateDebugSession(session => {
this.sessions.delete(session);
}),
debug.registerDebugAdapterTrackerFactory("*", {
createDebugAdapterTracker: session => {
const extendedSession = this.getDebugSession(session);
return {
onDidSendMessage: async msg => {
type Message =
| StoppedEvent
| ThreadsResponse
| ContinueLikeResponse;
interface ContinueLikeResponse {
type: "response";
command:
| "continue"
| "stepIn"
| "stepOut"
| "next";
}
interface StoppedEvent {
type: "event";
event: "stopped";
body: {
threadId: number;
};
}
interface ThreadsResponse {
type: "response";
command: "threads";
success: boolean;
body: {
threads: ThreadInfo[];
};
}
interface ThreadInfo {
id: number;
name: string;
}
const m = msg as Message;
if (m.type === "event") {
if (m.event === "stopped") {
const threadId = m.body.threadId;
const r = await extendedSession[
"getStackTrace"
]({
threadId,
startFrame: 0,
levels: 1,
});
extendedSession["activeStackFrame"] =
r.stackFrames.length > 0
? r.stackFrames[0].id
: undefined;
}
} else if (m.type === "response") {
if (
m.command === "continue" ||
m.command === "next" ||
m.command === "stepIn" ||
m.command === "stepOut"
) {
extendedSession[
"activeStackFrame"
] = undefined;
}
}
},
};
},
}),
]);
}
}
interface StackFrame {
id: number;
name: string;
}
export class VsCodeDebugSession {
@observable protected activeStackFrame: number | undefined;
constructor(public readonly session: DebugSession) {}
protected async getStackTrace(args: {
threadId: number;
startFrame?: number;
levels?: number;
}): Promise<{ totalFrames?: number; stackFrames: StackFrame[] }> {
try {
const reply = (await this.session.customRequest("stackTrace", {
threadId: args.threadId,
levels: args.levels,
startFrame: args.startFrame || 0,
})) as { totalFrames?: number; stackFrames: StackFrame[] };
return reply;
} catch (e) {
console.error(e);
throw e;
}
}
public async getCompletions(args: {
text: string;
column: number;
frameId: number | undefined;
}): Promise<CompletionItem[]> {
try {
const reply = await this.session.customRequest("completions", {
text: args.text,
frameId: args.frameId,
column: args.column,
});
if (!reply) {
return [];
}
return reply.targets;
} catch (error) {
console.error(error);
return [];
}
}
/**
* Evaluates the given expression.
* If context is "watch", long results are usually shortened.
*/
public async evaluate(args: {
expression: string;
frameId: number | undefined;
context: "watch" | "repl" | "copy";
}): Promise<{ result: string }> {
const reply = await this.session.customRequest("evaluate", {
expression: args.expression,
frameId: args.frameId,
context: args.context,
});
return { result: reply.result };
}
}
export class VsCodeDebuggerView {
public readonly dispose = Disposable.fn();
@observable private _activeDebugSession: VsCodeDebugSession | undefined;
public get activeDebugSession(): VsCodeDebugSession | undefined {
return this._activeDebugSession;
}
public get activeFrameId(): number | undefined {
if (!this._activeDebugSession) {
return undefined;
} else {
return this._activeDebugSession["activeStackFrame"];
}
}
constructor(private vsCodeDebugger: VsCodeDebugger) {
this.dispose.track(
debug.onDidChangeActiveDebugSession(activeSession => {
this.updateActiveDebugSession(activeSession);
})
);
this.updateActiveDebugSession(debug.activeDebugSession);
}
@action
private updateActiveDebugSession(activeSession: DebugSession | undefined) {
if (!activeSession) {
this._activeDebugSession = undefined;
} else {
const s = this.vsCodeDebugger.getDebugSession(activeSession);
this._activeDebugSession = s;
}
}
}

View File

@@ -0,0 +1,70 @@
import { DebugSession } from "vscode";
import { CompletionItem } from "../webviewContract";
import { observable } from "mobx";
export class EnhancedDebugSession {
@observable protected activeStackFrame: number | undefined;
constructor(public readonly session: DebugSession) {}
protected async getStackTrace(args: {
threadId: number;
startFrame?: number;
levels?: number;
}): Promise<{ totalFrames?: number; stackFrames: StackFrame[] }> {
try {
const reply = (await this.session.customRequest("stackTrace", {
threadId: args.threadId,
levels: args.levels,
startFrame: args.startFrame || 0,
})) as { totalFrames?: number; stackFrames: StackFrame[] };
return reply;
} catch (e) {
console.error(e);
throw e;
}
}
public async getCompletions(args: {
text: string;
column: number;
frameId: number | undefined;
}): Promise<CompletionItem[]> {
try {
const reply = await this.session.customRequest("completions", {
text: args.text,
frameId: args.frameId,
column: args.column,
});
if (!reply) {
return [];
}
return reply.targets;
} catch (error) {
console.error(error);
return [];
}
}
/**
* Evaluates the given expression.
* If context is "watch", long results are usually shortened.
*/
public async evaluate(args: {
expression: string;
frameId: number | undefined;
context: "watch" | "repl" | "copy";
}): Promise<{ result: string }> {
const reply = await this.session.customRequest("evaluate", {
expression: args.expression,
frameId: args.frameId,
context: args.context,
});
return { result: reply.result };
}
}
interface StackFrame {
id: number;
name: string;
}

View File

@@ -0,0 +1,116 @@
import { Disposable } from "@hediet/std/disposable";
import { debug, DebugSession } from "vscode";
import { EventEmitter } from "@hediet/std/events";
import { runInAction } from "mobx";
import { EnhancedDebugSession } from "./EnhancedDebugSession";
/**
* Decorates DebugSession instances and emits an event for new debug sessions.
*/
export class VsCodeDebugger {
public readonly dispose = Disposable.fn();
private readonly sessions = new Map<DebugSession, EnhancedDebugSession>();
private readonly _onDidStartDebugSession = new EventEmitter<{
session: EnhancedDebugSession;
}>();
public readonly onDidStartDebugSession = this._onDidStartDebugSession.asEvent();
public getEnhancedDebugSession(
session: DebugSession
): EnhancedDebugSession {
let result = this.sessions.get(session);
if (!result) {
result = new EnhancedDebugSession(session);
this.sessions.set(session, result);
}
return result;
}
constructor() {
this.dispose.track([
debug.onDidStartDebugSession(session => {
const e = this.sessions.get(session)!;
this._onDidStartDebugSession.emit({ session: e });
}),
debug.onDidTerminateDebugSession(session => {
this.sessions.delete(session);
}),
debug.registerDebugAdapterTrackerFactory("*", {
createDebugAdapterTracker: session => {
const extendedSession = this.getEnhancedDebugSession(
session
);
return {
onDidSendMessage: async msg => {
type Message =
| StoppedEvent
| ThreadsResponse
| ContinueLikeResponse;
interface ContinueLikeResponse {
type: "response";
command:
| "continue"
| "stepIn"
| "stepOut"
| "next";
}
interface StoppedEvent {
type: "event";
event: "stopped";
body: {
threadId: number;
};
}
interface ThreadsResponse {
type: "response";
command: "threads";
success: boolean;
body: {
threads: ThreadInfo[];
};
}
interface ThreadInfo {
id: number;
name: string;
}
const m = msg as Message;
if (m.type === "event") {
if (m.event === "stopped") {
const threadId = m.body.threadId;
const r = await extendedSession[
"getStackTrace"
]({
threadId,
startFrame: 0,
levels: 1,
});
extendedSession["activeStackFrame"] =
r.stackFrames.length > 0
? r.stackFrames[0].id
: undefined;
}
} else if (m.type === "response") {
if (
m.command === "continue" ||
m.command === "next" ||
m.command === "stepIn" ||
m.command === "stepOut"
) {
extendedSession[
"activeStackFrame"
] = undefined;
}
}
},
};
},
}),
]);
}
}

View File

@@ -0,0 +1,48 @@
import { Disposable } from "@hediet/std/disposable";
import { debug, DebugSession } from "vscode";
import { observable, action } from "mobx";
import { VsCodeDebugger } from "./VsCodeDebugger";
import { EnhancedDebugSession } from "./EnhancedDebugSession";
/**
* Tracks the currently active debug session and its currently active stack frame.
* This is currently all faked. A proper API would be nice.
*/
export class VsCodeDebuggerView {
public readonly dispose = Disposable.fn();
@observable private _activeDebugSession: EnhancedDebugSession | undefined;
public get activeDebugSession(): EnhancedDebugSession | undefined {
return this._activeDebugSession;
}
public get activeFrameId(): number | undefined {
if (!this._activeDebugSession) {
return undefined;
} else {
return this._activeDebugSession["activeStackFrame"];
}
}
constructor(private vsCodeDebugger: VsCodeDebugger) {
this.dispose.track(
debug.onDidChangeActiveDebugSession(activeSession => {
this.updateActiveDebugSession(activeSession);
})
);
this.updateActiveDebugSession(debug.activeDebugSession);
}
@action
private updateActiveDebugSession(activeSession: DebugSession | undefined) {
if (!activeSession) {
this._activeDebugSession = undefined;
} else {
const s = this.vsCodeDebugger.getEnhancedDebugSession(
activeSession
);
this._activeDebugSession = s;
}
}
}

View File

@@ -12,10 +12,11 @@ if (process.env.HOT_RELOAD) {
}
registerUpdateReconciler(module);
import { WebViews } from "./WebViews";
import { Server } from "./Server";
import { InternalWebviewManager } from "./webview/InternalWebviewManager";
import { WebviewServer } from "./webview/WebviewServer";
import { Config } from "./Config";
import { VsCodeDebugger, VsCodeDebuggerView } from "./VsCodeDebugger";
import { VsCodeDebugger } from "./debugger/VsCodeDebugger";
import { VsCodeDebuggerView } from "./debugger/VsCodeDebuggerView";
import { EvaluationWatchServiceImpl } from "./EvaluationWatchService";
import {
ComposedEvaluationEngine,
@@ -24,11 +25,18 @@ import {
ConfiguredEvaluationEngine,
} from "./EvaluationWatchService/EvaluationEngine";
export function activate(context: ExtensionContext) {
context.subscriptions.push(
hotRequireExportedFn(module, Extension, Extension => new Extension())
);
}
export function deactivate() {}
export class Extension {
public readonly dispose = Disposable.fn();
private readonly config = new Config();
private readonly debugger = this.dispose.track(new VsCodeDebugger());
private readonly debuggerView = this.dispose.track(
new VsCodeDebuggerView(this.debugger)
@@ -43,8 +51,10 @@ export class Extension {
])
);
private readonly server = new Server(this.dataSource, this.config);
private readonly views = this.dispose.track(new WebViews(this.server));
private readonly server = new WebviewServer(this.dataSource, this.config);
private readonly views = this.dispose.track(
new InternalWebviewManager(this.server, this.config)
);
constructor() {
if (getReloadCount(module) > 0) {
@@ -79,10 +89,10 @@ export class Extension {
}
const connections = [...this.server.connections.values()];
if (connections.length > 0) {
const latestConnection =
connections[connections.length - 1];
const latestConnection =
connections[connections.length - 1];
if (latestConnection) {
latestConnection.setExpression(selectedText);
} else {
this.views.createNew(selectedText);
@@ -92,11 +102,3 @@ export class Extension {
);
}
}
export function activate(context: ExtensionContext) {
context.subscriptions.push(
hotRequireExportedFn(module, Extension, Extension => new Extension())
);
}
export function deactivate() {}

View File

@@ -0,0 +1,120 @@
import { Uri, workspace, ConfigurationTarget, Disposable } from "vscode";
import { fromResource } from "mobx-utils";
import { computed, runInAction } from "mobx";
import { EventEmitter } from "@hediet/std/events";
export interface Serializer<T> {
deserialize: (val: any) => T;
serializer: (val: T) => any;
}
export function serializerWithDefault<T>(defaultValue: T): Serializer<T> {
return {
deserialize: val => (val === undefined ? defaultValue : val),
serializer: val => val,
};
}
export class VsCodeSetting<T> {
public get T(): T {
throw new Error();
}
public readonly serializer: Serializer<T>;
public readonly scope: Uri | undefined;
private readonly settingResource: VsCodeSettingResource;
public constructor(
public readonly id: string,
options: {
serializer?: Serializer<T>;
scope?: Uri;
} = {}
) {
this.scope = options.scope;
this.serializer = options.serializer || {
deserialize: val => val,
serializer: val => val,
};
this.settingResource = new VsCodeSettingResource(this.id, this.scope);
}
public get(): T {
const result = this.settingResource.value;
return this.serializer.deserialize(result);
}
public async set(value: T): Promise<void> {
const value2 = this.serializer.serializer(value);
const c = workspace.getConfiguration(undefined, this.scope);
const result = c.inspect(this.id);
let target: ConfigurationTarget;
if (
result &&
[result.workspaceFolderValue].some(i => i !== undefined)
) {
target = ConfigurationTarget.WorkspaceFolder;
}
if (result && [result.workspaceValue].some(i => i !== undefined)) {
target = ConfigurationTarget.Workspace;
} else {
target = ConfigurationTarget.Global;
}
await c.update(this.id, value2, target);
}
}
class VsCodeSettingResource {
public static onConfigChange = new EventEmitter();
private subscription: Disposable | undefined;
private readonly resource = fromResource<any>(
update => {
update(this.readValue());
this.subscription = VsCodeSettingResource.onConfigChange.sub(() => {
update(this.readValue());
});
},
() => this.subscription!.dispose(),
this.readValue()
);
constructor(
private readonly id: string,
private readonly scope: Uri | undefined
) {}
private readValue(): any {
const val = workspace
.getConfiguration(undefined, this.scope)
.get(this.id);
return val;
}
/**
* This improves change detection.
*/
private readonly stringifiedSettingValue = computed(
() => JSON.stringify(this.resource.current()),
{
name: `VsCodeSettingResource[${this.id}].value`,
context: this,
}
);
public get value() {
const v = this.stringifiedSettingValue.get();
if (v === undefined) {
return undefined;
}
return JSON.parse(v);
}
}
workspace.onDidChangeConfiguration(() => {
runInAction("Update Configuration", () => {
VsCodeSettingResource.onConfigChange.emit();
});
});

View File

@@ -1,15 +1,23 @@
import { window, ViewColumn, WebviewPanel } from "vscode";
import { Server } from "./Server";
import { WebviewServer } from "./WebviewServer";
import { Disposable } from "@hediet/std/disposable";
import { WindowWithWebviewData } from "../webviewContract";
import { Config } from "../Config";
export const debugVisualizer = "debugVisualizer";
export class WebViews {
private readonly debugVisualizations = new Map<WebviewPanel, WebView>();
export class InternalWebviewManager {
private readonly openedWebviews = new Map<
WebviewPanel,
DebugVisualizerWebview
>();
public readonly dispose = Disposable.fn();
constructor(private readonly server: Server) {
constructor(
private readonly server: WebviewServer,
private readonly config: Config
) {
this.dispose.track(
window.registerWebviewPanelSerializer(debugVisualizer, {
deserializeWebviewPanel: async (panel, state) => {
@@ -20,7 +28,7 @@ export class WebViews {
this.dispose.track({
dispose: () => {
for (const panel of this.debugVisualizations.keys()) {
for (const panel of this.openedWebviews.keys()) {
panel.dispose();
}
},
@@ -28,7 +36,7 @@ export class WebViews {
}
public createNew(expression: string | undefined = undefined) {
const panel = window.createWebviewPanel(
const webviewPanel = window.createWebviewPanel(
debugVisualizer,
"Debug Visualizer",
ViewColumn.Two,
@@ -44,33 +52,38 @@ export class WebViews {
}
);
this.setupView(panel, expression);
this.initializeView(webviewPanel, expression);
}
public restore(webviewPanel: WebviewPanel) {
this.setupView(webviewPanel);
private restore(webviewPanel: WebviewPanel) {
this.initializeView(webviewPanel);
}
private setupView(
private initializeView(
webviewPanel: WebviewPanel,
expression: string | undefined = undefined
) {
webviewPanel.webview.html = getWebviewHtml(this.server, expression);
const view = new WebView(webviewPanel);
this.debugVisualizations.set(webviewPanel, view);
webviewPanel.webview.html = getDebugVisualizerWebviewHtml(
this.server,
expression,
this.config
);
const view = new DebugVisualizerWebview(webviewPanel);
this.openedWebviews.set(webviewPanel, view);
webviewPanel.onDidDispose(() => {
this.debugVisualizations.delete(webviewPanel);
this.openedWebviews.delete(webviewPanel);
});
}
}
export class WebView {
export class DebugVisualizerWebview {
constructor(private readonly webviewPanel: WebviewPanel) {}
}
export function getWebviewHtml(
server: Server,
expression: string | undefined = undefined
function getDebugVisualizerWebviewHtml(
server: WebviewServer,
initialExpression: string | undefined = undefined,
config: Config
) {
const isDev = !!process.env.USE_DEV_UI;
return `
@@ -86,12 +99,15 @@ export function getWebviewHtml(
</head>
<body>
<script>
window.webViewData = ${JSON.stringify({
serverSecret: server.secret,
serverPort: server.port,
publicPath: server.publicPath,
expression,
})};
Object.assign(window, ${JSON.stringify({
webviewData: {
serverSecret: server.secret,
serverPort: server.port,
publicPath: server.publicPath,
expression: initialExpression,
theme: config.theme,
},
} as WindowWithWebviewData)});
const api = window.VsCodeApi = acquireVsCodeApi();
window.addEventListener('message', event => {
if (event.source === window.frames[0]) {
@@ -109,10 +125,13 @@ export function getWebviewHtml(
${
isDev
? `<iframe sandbox="allow-top-navigation allow-scripts allow-same-origin allow-popups allow-pointer-lock allow-forms" src="${server.getIndexUrl(
{ mode: "webViewIFrame", expression }
? `<iframe sandbox="allow-top-navigation allow-scripts allow-same-origin allow-popups allow-pointer-lock allow-forms" src="${server.getWebviewPageUrl(
{
mode: "webviewIFrame",
expression: initialExpression,
}
)}"></iframe>`
: `<script type="text/javascript" src="${server.mainBundleUrl}"></script>`
: `<script type="text/javascript" src="${server.webviewBundleUrl}"></script>`
}
</body>
</html>

View File

@@ -1,28 +1,28 @@
import { Disposable } from "@hediet/std/disposable";
import { debugVisualizerUIContract } from "./contract";
import { webviewContract } from "../webviewContract";
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
import {
EvaluationWatcher,
EvaluationWatchService,
} from "./EvaluationWatchService";
} from "../EvaluationWatchService";
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { observable, autorun } from "mobx";
import { Server } from "./Server";
import { WebviewServer } from "./WebviewServer";
import * as open from "open";
import chromeLauncher = require("chrome-launcher");
import { Config } from "./Config";
import { Config } from "../Config";
export class ClientConnection {
export class WebviewConnection {
public readonly dispose = Disposable.fn();
@observable
private watcher: EvaluationWatcher | undefined = undefined;
private readonly client: typeof debugVisualizerUIContract["TClientInterface"];
private readonly client: typeof webviewContract["TClientInterface"];
constructor(
evaluationWatchService: EvaluationWatchService,
stream: WebSocketStream,
server: Server,
server: WebviewServer,
config: Config,
serverSecret: string
) {
@@ -34,10 +34,7 @@ export class ClientConnection {
}
}
const {
client,
channel,
} = debugVisualizerUIContract.registerServerToStream(
const { client, channel } = webviewContract.registerServerToStream(
stream,
new ConsoleRpcLogger(),
{
@@ -76,7 +73,7 @@ export class ClientConnection {
openInBrowser: async ({}) => {
throwIfNotAuthenticated();
const url = server.getIndexUrl({
const url = server.getWebviewPageUrl({
mode: "standalone",
expression: this.watcher
? this.watcher.expression
@@ -141,6 +138,10 @@ export class ClientConnection {
public setExpression(expression: string) {
this.client.setExpression({ expression });
}
public setTheme(theme: "light" | "dark"): void {
this.client.setTheme({ theme });
}
}
async function launchChrome(url: string): Promise<boolean> {

View File

@@ -1,22 +1,27 @@
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { AddressInfo } from "net";
import WebSocket = require("ws");
import { ClientConnection } from "./ClientConnection";
import { WebviewConnection } from "./WebviewConnection";
import * as express from "express";
import * as http from "http";
import * as serveStatic from "serve-static";
import { Config } from "./Config";
import { Config } from "../Config";
import cryptoRandomString = require("crypto-random-string");
import { distPath } from "debug-visualizer-webview";
import { EvaluationWatchService } from "./EvaluationWatchService/EvaluationWatchService";
import { EvaluationWatchService } from "../EvaluationWatchService/EvaluationWatchService";
import { URLSearchParams } from "url";
import { autorun, reaction } from "mobx";
export class Server {
export class WebviewServer {
private readonly server: http.Server;
public readonly secret = cryptoRandomString({ length: 20 });
public readonly secret = cryptoRandomString({ length: 30 });
public readonly connections = new Set<ClientConnection>();
public readonly connections = new Set<WebviewConnection>();
constructor(dataSource: EvaluationWatchService, config: Config) {
constructor(
dataSource: EvaluationWatchService,
private readonly config: Config
) {
const app = express();
app.use(serveStatic(distPath));
@@ -32,7 +37,7 @@ export class Server {
const wss = new WebSocket.Server({ server: this.server });
wss.on("connection", async ws => {
const stream = new WebSocketStream(ws);
const c = new ClientConnection(
const c = new WebviewConnection(
dataSource,
stream,
this,
@@ -43,27 +48,42 @@ export class Server {
await stream.onClosed;
this.connections.delete(c);
});
reaction(
() => config.theme,
theme => {
for (const c of this.connections) {
c.setTheme(theme);
}
}
);
}
public getIndexUrl(args: {
public getWebviewPageUrl(args: {
expression?: string;
mode: "standalone" | "webViewIFrame";
mode: "standalone" | "webviewIFrame";
}): string {
const port = process.env.USE_DEV_UI ? 8080 : this.port;
const expr =
args.expression !== undefined
? `&expression=${encodeURIComponent(args.expression)}`
: "";
const inWebView =
args.mode === "standalone" ? "" : "&mode=webViewIFrame";
return `http://localhost:${port}/index.html?serverPort=${this.port}&serverSecret=${this.secret}${inWebView}${expr}`;
const port = process.env.USE_DEV_UI ? 8081 : this.port;
const params: Record<string, string> = {
serverPort: this.port.toString(),
serverSecret: this.secret,
mode: args.mode,
theme: this.config.theme,
};
if (args.expression !== undefined) {
params.expression = args.expression;
}
return `http://localhost:${port}/index.html?${new URLSearchParams(
params
).toString()}`;
}
public get publicPath(): string {
return `http://localhost:${this.port}/`;
}
public get mainBundleUrl(): string {
public get webviewBundleUrl(): string {
return `${this.publicPath}main.js`;
}

View File

@@ -9,6 +9,83 @@ import {
DataExtractorId,
} from "@hediet/debug-visualizer-data-extraction";
interface WebviewConfig {
serverSecret: string;
serverPort: number;
publicPath: string;
expression?: string;
theme: "light" | "dark";
}
export interface WebviewUrlParams {
serverPort: string;
serverSecret: string;
mode: "standalone" | "webviewIFrame";
expression: string;
theme: "light" | "dark";
}
export interface WindowWithWebviewData {
webviewData?: WebviewConfig;
}
export const webviewContract = contract({
client: {
updateState: notificationContract({
params: types.type({
newState: unchecked<DataExtractionState>(),
}),
}),
updateLanguageId: notificationContract({
params: types.type({
languageId: types.union([types.null, types.string]),
}),
}),
setExpression: requestContract({
params: types.type({
expression: types.string,
}),
}),
setTheme: notificationContract({
params: types.type({
theme: types.union([
types.literal("light"),
types.literal("dark"),
]),
}),
}),
},
server: {
authenticate: requestContract({
params: types.type({
secret: types.string,
}),
}),
setPreferredDataExtractor: notificationContract({
params: types.type({
dataExtractorId: unchecked<DataExtractorId>(),
}),
}),
setExpression: notificationContract({
params: types.type({
newExpression: types.string,
}),
}),
refresh: notificationContract({}),
openInBrowser: notificationContract({}),
getCompletions: requestContract({
params: types.type({
text: types.string,
column: types.number,
}),
result: types.type({
completions: types.array(unchecked<CompletionItem>()),
}),
}),
},
});
function unchecked<T>(): types.Type<T, any, unknown> {
return new types.Type<T, T, unknown>(
"unchecked",
@@ -92,52 +169,3 @@ export interface CompletionItem {
*/
length?: number;
}
export const debugVisualizerUIContract = contract({
client: {
updateState: notificationContract({
params: types.type({
newState: unchecked<DataExtractionState>(),
}),
}),
updateLanguageId: notificationContract({
params: types.type({
languageId: types.union([types.null, types.string]),
}),
}),
setExpression: requestContract({
params: types.type({
expression: types.string,
}),
}),
},
server: {
authenticate: requestContract({
params: types.type({
secret: types.string,
}),
}),
setPreferredDataExtractor: notificationContract({
params: types.type({
dataExtractorId: unchecked<DataExtractorId>(),
}),
}),
setExpression: notificationContract({
params: types.type({
newExpression: types.string,
}),
}),
refresh: notificationContract({}),
openInBrowser: notificationContract({}),
getCompletions: requestContract({
params: types.type({
text: types.string,
column: types.number,
}),
result: types.type({
completions: types.array(unchecked<CompletionItem>()),
}),
}),
},
});

View File

@@ -1,2 +0,0 @@
require("ts-node").register();
module.exports = require("./webpack.config.ts");

View File

@@ -10,13 +10,14 @@ export class App extends React.Component {
constructor(props: any) {
super(props);
if (this.model.runningMode !== "webView") {
if (this.model.runningMode !== "webview") {
if (this.model.theme === "light") {
require("../vscode-light.scss");
} else {
require("../vscode-dark.scss");
}
}
console.log(this.model.theme);
}
render() {

View File

@@ -4,7 +4,7 @@ import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction";
import { computed } from "mobx";
import { observer } from "mobx-react";
import { Model } from "../model/Model";
import { VisualizationId } from "@hediet/visualization";
import { VisualizationId } from "@hediet/visualization-core";
@observer
export class VisualizerHeaderDetails extends React.Component<{ model: Model }> {

View File

@@ -1,7 +1,9 @@
import {
debugVisualizerUIContract,
webviewContract,
DataExtractionState,
} from "debug-visualizer/src/contract";
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";
@@ -20,21 +22,14 @@ import { MonacoBridge } from "./MonacoBridge";
import { Disposable } from "@hediet/std/disposable";
import { startInterval, EventTimer } from "@hediet/std/timer";
declare const window: Window & {
webViewData?: {
serverSecret: string;
serverPort: number;
publicPath: string;
expression: string | undefined;
};
};
declare const window: Window & WindowWithWebviewData;
declare let __webpack_public_path__: string;
export class Model {
public readonly dispose = Disposable.fn();
public readonly runningMode: "standalone" | "webView" | "webViewIFrame" =
"webView";
public readonly runningMode: "standalone" | "webview" | "webviewIFrame" =
"webview";
@observable
public theme: "dark" | "light" = "light";
@@ -103,7 +98,7 @@ export class Model {
@observable.ref
public server:
| typeof debugVisualizerUIContract.TServerInterface
| typeof webviewContract.TServerInterface
| undefined = undefined;
private readonly vsCodeApi = getVsCodeApi<{ expression: string }>();
@@ -117,57 +112,46 @@ export class Model {
private readonly _bridge = new MonacoBridge(this);
constructor() {
if (window.webViewData) {
const data = window.webViewData;
if (window.webviewData) {
const data = window.webviewData;
this.port = data.serverPort;
this.serverSecret = data.serverSecret;
this.runningMode = "webView";
this.runningMode = "webview";
__webpack_public_path__ = data.publicPath;
const updateTheme = () => {
const isLight = document.body.classList.contains(
"vscode-light"
);
this.theme = isLight ? "light" : "dark";
};
updateTheme();
this.theme = window.webviewData.theme;
if (data.expression !== undefined) {
this.setExpression(data.expression);
}
this.dispose.track(
startInterval(1000, () => {
updateTheme();
})
);
} else {
const url = new URL(window.location.href);
const portStr = url.searchParams.get("serverPort");
const urlParams = (Object.fromEntries(
url.searchParams.entries()
) as unknown) as WebviewUrlParams;
const portStr = urlParams.serverPort;
if (!portStr) {
throw new Error("No port given.");
}
this.port = parseInt(portStr);
const expr = url.searchParams.get("expression");
const expr = urlParams.expression;
if (expr) {
this.setExpression(expr);
}
const secret = url.searchParams.get("serverSecret");
const secret = urlParams.serverSecret;
if (!secret) {
throw new Error("Server secret not set.");
}
this.serverSecret = secret;
const theme = url.searchParams.get("theme");
if (theme && theme === "dark") {
this.theme = "dark";
} else {
this.theme = "light";
const theme = urlParams.theme;
if (theme) {
this.theme = theme;
}
const mode = url.searchParams.get("mode");
const mode = urlParams.mode;
if (mode) {
this.runningMode = mode as any;
} else {
@@ -225,9 +209,7 @@ export class Model {
host: "localhost",
port: this.port,
});
const {
server,
} = debugVisualizerUIContract.getServerFromStream(
const { server } = webviewContract.getServerFromStream(
stream,
new ConsoleRpcLogger(),
{
@@ -245,6 +227,9 @@ export class Model {
updateLanguageId: async ({ languageId }) => {
this.languageId = languageId || "text";
},
setTheme: async ({ theme }) => {
this.theme = theme;
},
}
);
try {

View File

@@ -994,10 +994,10 @@
dependencies:
source-map "^0.6.1"
"@types/vscode@1.35.0":
version "1.35.0"
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.35.0.tgz#b17526ef1e9df5cbf7253be6765a33fd518a2304"
integrity sha512-Iyliuu8Hv4qy4TEaevQzChh9UsTEcuaKdcHXBbvJnoJSF5Td2yNENOrPK+vuOaXJJBhQZb4BNJKOxt6caaQR8A==
"@types/vscode@1.47.0":
version "1.47.0"
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.47.0.tgz#4a4051c21ecaadcf383a2c4387bea282540e96de"
integrity sha512-nJA37ykkz9FYA0ZOQUSc3OZnhuzEW2vUhUEo4MiduUo82jGwwcLfyvmgd/Q7b0WrZAAceojGhZybg319L24bTA==
"@types/webpack-sources@*":
version "0.1.6"
@@ -6624,6 +6624,11 @@ mobx-react@^6.1.8:
dependencies:
mobx-react-lite "^1.4.2"
mobx-utils@^5.6.1:
version "5.6.1"
resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-5.6.1.tgz#b7d9184b7442fe704be367d4363a2e9961be28cc"
integrity sha512-bpTJzM8MXniGnXCZY+ImjPDqBKQ3+G3g/QFSPtNkH6HM3x14DAPqKH7No7NDyhbXBMv3FaVetsgnoEPozbi45Q==
mobx@^5.10.0:
version "5.13.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.13.0.tgz#0fd68f10aa5ff2d146a4ed9e145b53337cfbca59"