diff --git a/cyberbrain-vsc/src/interactions.ts b/cyberbrain-vsc/src/interactions.ts new file mode 100644 index 00000000..cc8d7243 --- /dev/null +++ b/cyberbrain-vsc/src/interactions.ts @@ -0,0 +1,86 @@ +import { + window, + Range, + Position, + workspace, + TextEditor, + OverviewRulerLane, + TextEditorDecorationType +} from "vscode"; + +enum Behavior { + Hover = "Hover", + Unhover = "Unhover" +} + +/** + * `Interactions` is used to implement the interaction behaviors from users when they interact with cyberbrain webpages. + * To add an interation, post message to Vscode when a user's interaction behavior is detected, parse the message with rpc server, + * define relative interactions logic in the `Interactions`, and then call `Interactions.excute` to excute it. + * Current Interaction Behaviors includes: + * 1. Highlight the line of the node in the editor when hovering the mouse on any node. Cancel the highlight when the node is unhovered. + **/ +export class Interactions { + private decorateType: TextEditorDecorationType | undefined; + + getDecorationType(): TextEditorDecorationType { + return window.createTextEditorDecorationType({ + isWholeLine: true, + overviewRulerLane: OverviewRulerLane.Right, + light: { + // this color will be used in light color themes + backgroundColor: "#fcf29f", + overviewRulerColor: "#fcf29f" + }, + dark: { + // this color will be used in dark color themes + backgroundColor: "#264F78", + overviewRulerColor: "#264F78" + } + }); + } + + /** + * Highlight the given line on the Editor. + * @param lineno the highlighted line's number. + * @param relativePath the relative path of the file which the highlighted line belongs to. + */ + highlightLineOnEditor(lineno: number, relativePath: string) { + const nodeEditor = window.visibleTextEditors.filter( + editor => editor.document.uri.fsPath.indexOf(relativePath) !== -1 + )[0]; + if (nodeEditor) { + // create a decoration type everytime since it will be disposed when unhovering the node + this.decorateType = this.getDecorationType(); + let lineRange = [ + { + range: new Range( + new Position(lineno - 1, 0), + new Position(lineno - 1, 100) + ) + } + ]; + nodeEditor.setDecorations(this.decorateType, lineRange); + } + } + + execute(interactionConfig: { type: String; info?: any }) { + switch (interactionConfig.type) { + case Behavior.Hover: + if (interactionConfig.info?.hasOwnProperty("lineno")) { + this.highlightLineOnEditor( + interactionConfig.info["lineno"], + interactionConfig.info["relativePath"] + ); + } + break; + case Behavior.Unhover: + if (this.decorateType) { + this.decorateType.dispose(); + } + break; + default: + break; + } + } +} diff --git a/cyberbrain-vsc/src/rpc_server.ts b/cyberbrain-vsc/src/rpc_server.ts index d50af7f9..b19ee0f6 100644 --- a/cyberbrain-vsc/src/rpc_server.ts +++ b/cyberbrain-vsc/src/rpc_server.ts @@ -4,6 +4,7 @@ import * as bodyParser from "body-parser"; import { openTraceGraph } from "./webview"; import { isTestMode } from "./utils"; import { decode } from "@msgpack/msgpack"; +import { Interactions } from "./interactions"; let cl = console.log; @@ -14,11 +15,13 @@ export class RpcServer { private server: express.Express; private readonly context: vscode.ExtensionContext; private readonly listeningPort = 1989; // TODO: Make it configurable. - + private interactions: Interactions; + constructor(context: vscode.ExtensionContext) { this.context = context; this.server = express(); this.server.use(bodyParser.raw({ limit: "10GB" })); + this.interactions = new Interactions(); this.server.post("/frame", (req, res) => { cl("get message"); @@ -28,8 +31,11 @@ export class RpcServer { // Sends data to the trace graph in webview. let webviewPanel = openTraceGraph(this.context); webviewPanel.webview.onDidReceiveMessage( - (message: string) => { - if (message === "Webview ready") { + (message: { + command: string; + interactionConfig?: { type: string; info?: any }; + }) => { + if (message.command === "Webview ready") { webviewPanel.webview.postMessage(decode(req.body)); // If under test, don't open the devtools window because it will cover the trace graph. @@ -39,6 +45,15 @@ export class RpcServer { ); } } + if (message.command === "Interaction behavior") { + try { + if (message.interactionConfig) { + this.interactions.execute(message.interactionConfig); + } + } catch (error) { + cl("Failed to execute the interation behavior: ", error); + } + } }, undefined, this.context.subscriptions diff --git a/cyberbrain-vsc/src/trace_graph.js b/cyberbrain-vsc/src/trace_graph.js index 9aea88c4..938e98c7 100644 --- a/cyberbrain-vsc/src/trace_graph.js +++ b/cyberbrain-vsc/src/trace_graph.js @@ -294,11 +294,29 @@ class TraceGraph { return; } this.hoveredNodeId = node.id; + + // highlight the current hoverd node's line on the editor + vscode.postMessage({ + command: "Interaction behavior", + interactionConfig: { + type: "Hover", + info: { lineno: node.lineno, relativePath: node.filename } + } + }); + // TODO: show values of all local variables at this point. displayValueInConsole(node); }); this.network.on("blurNode", event => { + // unhighlight the current hoverd node's line on the editor + vscode.postMessage({ + command: "Interaction behavior", + interactionConfig: { + type: "Unhover" + } + }); + this.hoveredNodeId = undefined; }); @@ -338,6 +356,7 @@ class TraceGraph { level: this.traceData.linenoMapping.get(event.lineno), lineno: event.lineno, label: buildLabelText(event), + filename: this.traceData.frameMetadata.filename, target: event.target, // "value" is a reserved property, use "runtimeValue" instead. runtimeValue: event.value, diff --git a/cyberbrain-vsc/src/webview.ts b/cyberbrain-vsc/src/webview.ts index 86c84b5d..8158a223 100644 --- a/cyberbrain-vsc/src/webview.ts +++ b/cyberbrain-vsc/src/webview.ts @@ -103,7 +103,7 @@ export function openTraceGraph(
`;