From 42e58ce9cfeefbbf0701c7e3482e9f975bb6e3a8 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Sun, 29 Mar 2020 15:16:46 +0200 Subject: [PATCH] * Implements languageId support * Implements typing of helper functions * Shift+F1 opens a new debug visualizer window if none is opened yet --- .vscode/launch.json | 9 ++- data-extraction/test/main.test.ts | 5 +- extension/src/ClientConnection.ts | 26 ++++++--- .../EvaluationEngine/EvaluationEngine.ts | 5 ++ .../GenericEvaluationEngine.ts | 2 + .../EvaluationEngine/JsEvaluationEngine.ts | 2 + .../EvaluationWatchService.ts | 7 +++ .../EvaluationWatchServiceImpl.ts | 25 +++++++-- extension/src/WebViews.ts | 19 +++++-- extension/src/contract.ts | 5 ++ extension/src/extension.ts | 3 +- webview/src/components/ExpressionInput.tsx | 7 ++- webview/src/model/Model.ts | 37 +++++++++--- webview/src/model/MonacoBridge.ts | 56 ++++++++++++++----- 14 files changed, 158 insertions(+), 50 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1e926e1..12d5738 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,13 @@ { "version": "0.2.0", "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome", + "url": "http://localhost:8090", + "webRoot": "${workspaceFolder}/visualization-playground" + }, { "name": "Run Extension (Hot Reload)", "type": "extensionHost", @@ -8,7 +15,7 @@ "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}/extension", - "${workspaceFolder}\\demos\\js" + "${workspaceFolder}\\demos\\php" ], "env": { "HOT_RELOAD": "true", diff --git a/data-extraction/test/main.test.ts b/data-extraction/test/main.test.ts index f5afa50..2dd4add 100644 --- a/data-extraction/test/main.test.ts +++ b/data-extraction/test/main.test.ts @@ -1,7 +1,4 @@ -import { - getDataExtractorApi, - DataExtractorApiImpl, -} from "@hediet/debug-visualizer-data-extraction"; +import { DataExtractorApiImpl } from "../"; describe("extractor", () => { it("should not crash", () => { diff --git a/extension/src/ClientConnection.ts b/extension/src/ClientConnection.ts index dad0618..5f1b542 100644 --- a/extension/src/ClientConnection.ts +++ b/extension/src/ClientConnection.ts @@ -20,7 +20,7 @@ export class ClientConnection { private readonly client: typeof debugVisualizerUIContract["TClientInterface"]; constructor( - dataSource: EvaluationWatchService, + evaluationWatchService: EvaluationWatchService, stream: WebSocketStream, server: Server, config: Config, @@ -65,9 +65,12 @@ export class ClientConnection { this.dispose.untrack(this.watcher).dispose(); } this.watcher = this.dispose.track( - dataSource.createEvaluationWatcher(newExpression, { - preferredDataExtractor: oldPreferredDataExtractor, - }) + evaluationWatchService.createEvaluationWatcher( + newExpression, + { + preferredDataExtractor: oldPreferredDataExtractor, + } + ) ); }, openInBrowser: async ({}) => { @@ -98,7 +101,7 @@ export class ClientConnection { getCompletions: async ({ text, column }) => { throwIfNotAuthenticated(); - const completions = await dataSource.getCompletions( + const completions = await evaluationWatchService.getCompletions( text, column ); @@ -111,7 +114,7 @@ export class ClientConnection { this.client = client; - this.dispose.track( + this.dispose.track([ Disposable.create( autorun(() => { if (this.watcher) { @@ -120,8 +123,15 @@ export class ClientConnection { }); } }) - ) - ); + ), + Disposable.create( + autorun(() => { + client.updateLanguageId({ + languageId: evaluationWatchService.languageId || null, + }); + }) + ), + ]); stream.onClosed.then(() => { this.dispose(); diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts index c3f4643..7587035 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts @@ -16,6 +16,11 @@ export interface Evaluator { | { kind: "data"; result: DataExtractionResult } | { kind: "error"; message: FormattedMessage } >; + + /** + * The language that expressions must be written in. + */ + readonly languageId: string | undefined; } export interface EvaluationArgs { diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts index 8b121cb..2fa425b 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts @@ -22,6 +22,8 @@ export class GenericEvaluationEngine implements EvaluationEngine { } export class GenericEvaluator implements Evaluator { + public readonly languageId = "text"; + constructor(private readonly session: VsCodeDebugSession) {} public async evaluate({ diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts index d8b8ccf..4fe2298 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts @@ -33,6 +33,8 @@ export class JsEvaluationEngine implements EvaluationEngine { } class JsEvaluator implements Evaluator { + public readonly languageId = "javascript"; + constructor(private readonly session: VsCodeDebugSession) {} public async evaluate({ diff --git a/extension/src/EvaluationWatchService/EvaluationWatchService.ts b/extension/src/EvaluationWatchService/EvaluationWatchService.ts index c59206d..c373e01 100644 --- a/extension/src/EvaluationWatchService/EvaluationWatchService.ts +++ b/extension/src/EvaluationWatchService/EvaluationWatchService.ts @@ -8,6 +8,13 @@ export interface EvaluationWatchService { ): EvaluationWatcher; getCompletions(text: string, column: number): Promise; + + /** + * The language the expressions must be written in. + * `undefined`, if unknown. + * This field is observable. + */ + readonly languageId: string | undefined; } export interface EvaluationWatcherOptions { diff --git a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts index 394cc9a..1ebae08 100644 --- a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts +++ b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts @@ -18,7 +18,7 @@ export class EvaluationWatchServiceImpl implements EvaluationWatchService { constructor( private readonly vsCodeDebuggerView: VsCodeDebuggerView, - private readonly dataExtractionProviderFactory: EvaluationEngine + private readonly evaluationEngine: EvaluationEngine ) { this.dispose.track({ dispose: autorun(() => { @@ -48,6 +48,21 @@ export class EvaluationWatchServiceImpl implements EvaluationWatchService { this.watchers.delete(w); } + private lastLanguageId: string | undefined = undefined; + + get languageId(): string | undefined { + const session = this.vsCodeDebuggerView.activeDebugSession; + if (!session) { + return this.lastLanguageId; + } + const evaluator = this.evaluationEngine.createEvaluator(session); + if (!evaluator) { + return this.lastLanguageId; + } + this.lastLanguageId = evaluator.languageId; + return evaluator.languageId; + } + public async refresh(w: ObservableEvaluationWatcher): Promise { const session = this.vsCodeDebuggerView.activeDebugSession; if (!session) { @@ -59,10 +74,8 @@ export class EvaluationWatchServiceImpl implements EvaluationWatchService { w._state = { kind: "loading" }; - const extractionProvider = this.dataExtractionProviderFactory.createEvaluator( - session - ); - if (!extractionProvider) { + const evaluator = this.evaluationEngine.createEvaluator(session); + if (!evaluator) { w._state = { kind: "error", message: `The debug adapter "${session.session.type}" is not supported.`, @@ -70,7 +83,7 @@ export class EvaluationWatchServiceImpl implements EvaluationWatchService { return; } - const result = await extractionProvider.evaluate({ + const result = await evaluator.evaluate({ expression: w.expression, frameId, preferredExtractorId: w.preferredDataExtractor, diff --git a/extension/src/WebViews.ts b/extension/src/WebViews.ts index 52ce878..3ca1bba 100644 --- a/extension/src/WebViews.ts +++ b/extension/src/WebViews.ts @@ -27,7 +27,7 @@ export class WebViews { }); } - public createNew() { + public createNew(expression: string | undefined = undefined) { const panel = window.createWebviewPanel( debugVisualizer, "Debug Visualizer", @@ -35,15 +35,18 @@ export class WebViews { { enableScripts: true } ); - this.setupView(panel); + this.setupView(panel, expression); } public restore(webviewPanel: WebviewPanel) { this.setupView(webviewPanel); } - private setupView(webviewPanel: WebviewPanel) { - webviewPanel.webview.html = getHtml(this.server); + private setupView( + webviewPanel: WebviewPanel, + expression: string | undefined = undefined + ) { + webviewPanel.webview.html = getHtml(this.server, expression); const view = new WebView(webviewPanel); this.debugVisualizations.set(webviewPanel, view); webviewPanel.onDidDispose(() => { @@ -56,7 +59,10 @@ export class WebView { constructor(private readonly webviewPanel: WebviewPanel) {} } -export function getHtml(server: Server) { +export function getHtml( + server: Server, + expression: string | undefined = undefined +) { const isDev = !!process.env.USE_DEV_UI; return ` @@ -75,6 +81,7 @@ export function getHtml(server: Server) { serverSecret: server.secret, serverPort: server.port, publicPath: server.publicPath, + expression, })}; const api = window.VsCodeApi = acquireVsCodeApi(); window.addEventListener('message', event => { @@ -94,7 +101,7 @@ export function getHtml(server: Server) { ${ isDev ? `` : `` } diff --git a/extension/src/contract.ts b/extension/src/contract.ts index 060ba1e..c0472cf 100644 --- a/extension/src/contract.ts +++ b/extension/src/contract.ts @@ -100,6 +100,11 @@ export const debugVisualizerUIContract = contract({ newState: unchecked(), }), }), + updateLanguageId: notificationContract({ + params: types.type({ + languageId: types.union([types.null, types.string]), + }), + }), setExpression: requestContract({ params: types.type({ expression: types.string, diff --git a/extension/src/extension.ts b/extension/src/extension.ts index f0a7921..4b11ad7 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -85,8 +85,7 @@ export class Extension { latestConnection.setExpression(selectedText); } else { - // TODO pass expression - // this.views.createNew(); + this.views.createNew(selectedText); } } ) diff --git a/webview/src/components/ExpressionInput.tsx b/webview/src/components/ExpressionInput.tsx index 1547d2d..a250390 100644 --- a/webview/src/components/ExpressionInput.tsx +++ b/webview/src/components/ExpressionInput.tsx @@ -10,7 +10,7 @@ export class ExpressionInput extends React.Component<{ model: Model }> { @observable private contentHeight: number | undefined = undefined; private model = monaco.editor.createModel( "", - "text", + "javascript", monaco.Uri.parse(`file:///main.ts`) ); @@ -54,6 +54,11 @@ export class ExpressionInput extends React.Component<{ model: Model }> { } }); + @disposeOnUnmount + private _updateLanguageId = autorun(() => { + monaco.editor.setModelLanguage(this.model, this.props.model.languageId); + }); + private readonly setEditorDiv = (editorDiv: HTMLDivElement) => { if (!editorDiv) { return; diff --git a/webview/src/model/Model.ts b/webview/src/model/Model.ts index 65bd693..112c84a 100644 --- a/webview/src/model/Model.ts +++ b/webview/src/model/Model.ts @@ -4,8 +4,11 @@ import { } from "debug-visualizer/src/contract"; import { WebSocketStream } from "@hediet/typed-json-rpc-websocket"; import { ConsoleRpcLogger } from "@hediet/typed-json-rpc"; -import { observable, action, computed, when } from "mobx"; -import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction"; +import { observable, action, computed, when, runInAction } from "mobx"; +import { + DataExtractorId, + ExtractedData, +} from "@hediet/debug-visualizer-data-extraction"; import { Visualization, VisualizationId, @@ -21,6 +24,7 @@ declare const window: Window & { serverSecret: string; serverPort: number; publicPath: string; + expression: string | undefined; }; }; @@ -38,10 +42,15 @@ export class Model { private serverSecret: string; @observable expression: string = ""; - @observable state: DataExtractionState | { kind: "noExpression" } = { + @observable state: + | DataExtractionState + | { kind: "noExpression" } + | { kind: "visualizationError"; data: ExtractedData } = { kind: "noExpression", }; + @observable languageId: string = "text"; + @observable private preferredVisualizationId: | VisualizationId | undefined = undefined; @@ -51,6 +60,11 @@ export class Model { this.preferredVisualizationId = id; } + @action + public setVisualizationError(data: ExtractedData) { + this.state = { kind: "visualizationError", data: data }; + } + @computed get visualizations(): | { visualization: Visualization | undefined; @@ -99,6 +113,10 @@ export class Model { }; updateTheme(); + if (data.expression !== undefined) { + this.setExpression(data.expression); + } + this.dispose.track( startInterval(1000, () => { updateTheme(); @@ -191,14 +209,19 @@ export class Model { new ConsoleRpcLogger(), { updateState: async ({ newState }) => { - this._loading = newState.kind === "loading"; - if (!this._loading) { - this.state = newState; - } + runInAction(() => { + this._loading = newState.kind === "loading"; + if (!this._loading) { + this.state = newState; + } + }); }, setExpression: async ({ expression }) => { this.setExpression(expression); }, + updateLanguageId: async ({ languageId }) => { + this.languageId = languageId || "text"; + }, } ); try { diff --git a/webview/src/model/MonacoBridge.ts b/webview/src/model/MonacoBridge.ts index 537797a..55d9ee3 100644 --- a/webview/src/model/MonacoBridge.ts +++ b/webview/src/model/MonacoBridge.ts @@ -2,13 +2,23 @@ import * as monaco from "monaco-editor"; import { Model } from "./Model"; import { Disposable } from "@hediet/std/disposable"; import { autorun } from "mobx"; + +declare const require: { + (path: string): { default: string }; + context: ( + path: string, + includeSubDirs: boolean, + regex: RegExp + ) => { (fileName: string): { default: string }; keys(): string[] }; +}; + export class MonacoBridge { public readonly dispose = Disposable.fn(); constructor(private readonly model: Model) { monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: false, - noSyntaxValidation: false, + noSemanticValidation: true, + noSyntaxValidation: true, }); monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES5, @@ -19,29 +29,45 @@ export class MonacoBridge { strict: true, }); - const es5Lib = require("!!raw-loader!./lib.es5.d.ts.txt") - .default as string; - const commonTypes = require("!!raw-loader!@hediet/debug-visualizer-data-extraction/src/CommonDataTypes.ts") - .default as string; + const es5Lib = require("!!raw-loader!./lib.es5.d.ts.txt").default; + const commonTypes = require.context( + "!!raw-loader!@hediet/debug-visualizer-data-extraction/dist/src/", + true, + /.*.d.ts$/ + ); + + for (const file of commonTypes.keys()) { + console.log(file); + this.dispose.track( + monaco.languages.typescript.javascriptDefaults.addExtraLib( + commonTypes(file).default, + `file:///node_modules/debug-visualizer-data-extraction/${file}` + ) + ); + } this.dispose.track([ monaco.languages.typescript.javascriptDefaults.addExtraLib(es5Lib), monaco.languages.typescript.javascriptDefaults.addExtraLib( - commonTypes, - `file:///node_modules/CommonDataTypes/index.ts` - ), - monaco.languages.typescript.javascriptDefaults.addExtraLib( - `declare function $asData(data: import("CommonDataTypes").CommonDataType): import("CommonDataTypes").CommonDataType;`, + `declare const hedietDbgVis: typeof import("debug-visualizer-data-extraction/js/helpers")`, `file:///types.d.ts` ), ]); - this.dispose.track( + const debugSessionCompletionProvider = new DebugSessionCompletionProvider( + this.model + ); + + this.dispose.track([ monaco.languages.registerCompletionItemProvider( "javascript", - new DebugSessionCompletionProvider(this.model) - ) - ); + debugSessionCompletionProvider + ), + monaco.languages.registerCompletionItemProvider( + "text", + debugSessionCompletionProvider + ), + ]); this.dispose.track({ dispose: autorun(() => {