diff --git a/extension/package.json b/extension/package.json index f732050..56bdee0 100644 --- a/extension/package.json +++ b/extension/package.json @@ -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", diff --git a/extension/src/Config.ts b/extension/src/Config.ts index 4aa7d1a..6aed039 100644 --- a/extension/src/Config.ts +++ b/extension/src/Config.ts @@ -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(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({}) } + ); + + @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(useChromeKioskModeKey), - true - ); - - this._debugAdapterConfigs = mapUndefined( - c.get(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(val: T | undefined, defaultVal: T) { - if (val === undefined) { - return defaultVal; - } - return val; -} - type DebugAdapterConfigs = { [debugAdapter: string]: { context?: "watch" | "repl"; diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts index 61ca35d..9563aa8 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts @@ -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) { diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts index dc09ea5..65a828f 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts @@ -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); diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts index 7587035..166acb5 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts @@ -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 { diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts index 340b815..40e08d5 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts @@ -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, diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts index dc95c6f..024cca6 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts @@ -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-")) { diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts b/extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts index 1df094c..1a31459 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts @@ -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; diff --git a/extension/src/EvaluationWatchService/EvaluationWatchService.ts b/extension/src/EvaluationWatchService/EvaluationWatchService.ts index c373e01..4ccf6f3 100644 --- a/extension/src/EvaluationWatchService/EvaluationWatchService.ts +++ b/extension/src/EvaluationWatchService/EvaluationWatchService.ts @@ -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( diff --git a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts index 1ebae08..537cd60 100644 --- a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts +++ b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts @@ -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) diff --git a/extension/src/VsCodeDebugger.ts b/extension/src/VsCodeDebugger.ts deleted file mode 100644 index df2e897..0000000 --- a/extension/src/VsCodeDebugger.ts +++ /dev/null @@ -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(); - - 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 { - 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; - } - } -} diff --git a/extension/src/debugger/EnhancedDebugSession.ts b/extension/src/debugger/EnhancedDebugSession.ts new file mode 100644 index 0000000..4cf7a3c --- /dev/null +++ b/extension/src/debugger/EnhancedDebugSession.ts @@ -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 { + 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; +} diff --git a/extension/src/debugger/VsCodeDebugger.ts b/extension/src/debugger/VsCodeDebugger.ts new file mode 100644 index 0000000..4880a7a --- /dev/null +++ b/extension/src/debugger/VsCodeDebugger.ts @@ -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(); + + 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; + } + } + }, + }; + }, + }), + ]); + } +} diff --git a/extension/src/debugger/VsCodeDebuggerView.ts b/extension/src/debugger/VsCodeDebuggerView.ts new file mode 100644 index 0000000..bf864b0 --- /dev/null +++ b/extension/src/debugger/VsCodeDebuggerView.ts @@ -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; + } + } +} diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 4b11ad7..2d680d1 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -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() {} diff --git a/extension/src/utils/VsCodeSettings.ts b/extension/src/utils/VsCodeSettings.ts new file mode 100644 index 0000000..8bbe0b5 --- /dev/null +++ b/extension/src/utils/VsCodeSettings.ts @@ -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 { + deserialize: (val: any) => T; + serializer: (val: T) => any; +} + +export function serializerWithDefault(defaultValue: T): Serializer { + return { + deserialize: val => (val === undefined ? defaultValue : val), + serializer: val => val, + }; +} + +export class VsCodeSetting { + public get T(): T { + throw new Error(); + } + + public readonly serializer: Serializer; + public readonly scope: Uri | undefined; + private readonly settingResource: VsCodeSettingResource; + + public constructor( + public readonly id: string, + options: { + serializer?: Serializer; + 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 { + 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( + 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(); + }); +}); diff --git a/extension/src/WebViews.ts b/extension/src/webview/InternalWebviewManager.ts similarity index 62% rename from extension/src/WebViews.ts rename to extension/src/webview/InternalWebviewManager.ts index 25e4b1d..a8f69e7 100644 --- a/extension/src/WebViews.ts +++ b/extension/src/webview/InternalWebviewManager.ts @@ -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(); +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( ` + : `` } diff --git a/extension/src/ClientConnection.ts b/extension/src/webview/WebviewConnection.ts similarity index 86% rename from extension/src/ClientConnection.ts rename to extension/src/webview/WebviewConnection.ts index 5f1b542..094db82 100644 --- a/extension/src/ClientConnection.ts +++ b/extension/src/webview/WebviewConnection.ts @@ -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 { diff --git a/extension/src/Server.ts b/extension/src/webview/WebviewServer.ts similarity index 50% rename from extension/src/Server.ts rename to extension/src/webview/WebviewServer.ts index 83d0b4e..28dd038 100644 --- a/extension/src/Server.ts +++ b/extension/src/webview/WebviewServer.ts @@ -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(); + public readonly connections = new Set(); - 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 = { + 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`; } diff --git a/extension/src/contract.ts b/extension/src/webviewContract.ts similarity index 83% rename from extension/src/contract.ts rename to extension/src/webviewContract.ts index c0472cf..fb6b9c7 100644 --- a/extension/src/contract.ts +++ b/extension/src/webviewContract.ts @@ -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(), + }), + }), + 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(), + }), + }), + 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()), + }), + }), + }, +}); + function unchecked(): types.Type { return new types.Type( "unchecked", @@ -92,52 +169,3 @@ export interface CompletionItem { */ length?: number; } - -export const debugVisualizerUIContract = contract({ - client: { - updateState: notificationContract({ - params: types.type({ - newState: unchecked(), - }), - }), - 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(), - }), - }), - 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()), - }), - }), - }, -}); diff --git a/extension/webpack.config.js b/extension/webpack.config.js deleted file mode 100644 index 510beb3..0000000 --- a/extension/webpack.config.js +++ /dev/null @@ -1,2 +0,0 @@ -require("ts-node").register(); -module.exports = require("./webpack.config.ts"); diff --git a/webview/src/components/App.tsx b/webview/src/components/App.tsx index c455c22..bc160fa 100644 --- a/webview/src/components/App.tsx +++ b/webview/src/components/App.tsx @@ -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() { diff --git a/webview/src/components/VisualizerHeaderDetails.tsx b/webview/src/components/VisualizerHeaderDetails.tsx index e416a25..cf51674 100644 --- a/webview/src/components/VisualizerHeaderDetails.tsx +++ b/webview/src/components/VisualizerHeaderDetails.tsx @@ -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 }> { diff --git a/webview/src/model/Model.ts b/webview/src/model/Model.ts index fc9ef6d..574c4b1 100644 --- a/webview/src/model/Model.ts +++ b/webview/src/model/Model.ts @@ -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 { diff --git a/yarn.lock b/yarn.lock index 1551f4b..63da74a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"