Skip to content

Commit 1c85379

Browse files
committed
Refactor messageHandler: singleton
1 parent 69d7bb2 commit 1c85379

File tree

3 files changed

+150
-148
lines changed

3 files changed

+150
-148
lines changed

src/commands/evalEditor.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import * as vscode from "vscode";
22
import Messages from "../utils/messages";
3-
import { activeEditor, sendToFrontendWrapped } from "./showPanel";
3+
import { sendToFrontendWrapped } from "./showPanel";
4+
import { MessageHandler } from "../utils/messageHandler";
5+
6+
let messageHandler = MessageHandler.getInstance();
47

58
export async function evalEditor(context: vscode.ExtensionContext) {
6-
if (!activeEditor) {
9+
if (!messageHandler.activeEditor) {
710
vscode.window.showErrorMessage(
811
"Cannot evaluate code when there is no active editor!",
912
);
1013
return;
1114
}
12-
const message = Messages.EvalEditor(activeEditor.workspaceLocation);
15+
const message = Messages.EvalEditor(
16+
messageHandler.activeEditor.workspaceLocation,
17+
);
1318
sendToFrontendWrapped(message);
1419
}

src/commands/showPanel.tsx

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,10 @@ import { setWebviewContent } from "../utils/webview";
1313
import config from "../utils/config";
1414
import { Editor } from "../utils/editor";
1515
import { FRONTEND_ELEMENT_ID } from "../constants";
16-
import { client } from "../extension";
1716
import _ from "lodash";
18-
import { McqPanelWithLogging as McqPanel } from "../webview/components/McqPanel";
19-
import { handleMessage } from "../utils/messageHandler";
17+
import { MessageHandler } from "../utils/messageHandler";
2018

21-
let mcqPanel: vscode.WebviewPanel | null = null;
22-
23-
let panel: vscode.WebviewPanel | null = null;
24-
// This needs to be a reference to active
25-
// TODO: Fix this ugly code!
26-
export let activeEditor: Editor | null = null;
19+
let messageHandler = MessageHandler.getInstance();
2720

2821
export async function showPanel(context: vscode.ExtensionContext) {
2922
let language: string | undefined = context.workspaceState.get("language");
@@ -34,7 +27,7 @@ export async function showPanel(context: vscode.ExtensionContext) {
3427
// Get a reference to the active editor (before the focus is switched to our newly created webview)
3528
// firstEditor = vscode.window.activeTextEditor!;
3629

37-
panel = vscode.window.createWebviewPanel(
30+
messageHandler.panel = vscode.window.createWebviewPanel(
3831
"source-academy-panel",
3932
"Source Academy",
4033
vscode.ViewColumn.Beside,
@@ -44,16 +37,16 @@ export async function showPanel(context: vscode.ExtensionContext) {
4437
},
4538
);
4639

47-
panel.webview.onDidReceiveMessage(
48-
(message: MessageType) => handleMessage(context, message),
40+
messageHandler.panel.webview.onDidReceiveMessage(
41+
(message: MessageType) => messageHandler.handleMessage(context, message),
4942
undefined,
5043
context.subscriptions,
5144
);
5245

5346
const frontendUrl = new URL("/playground", config.frontendBaseUrl).href;
5447

5548
setWebviewContent(
56-
panel,
49+
messageHandler.panel,
5750
context,
5851
// NOTE: This is not React code, but our FakeReact!
5952
<div
@@ -73,17 +66,17 @@ export async function showPanel(context: vscode.ExtensionContext) {
7366
</div>,
7467
);
7568

76-
panel.iconPath = vscode.Uri.joinPath(
69+
messageHandler.panel.iconPath = vscode.Uri.joinPath(
7770
context.extensionUri,
7871
"assets",
7972
"icon.png",
8073
);
8174
}
8275

8376
export async function sendToFrontendWrapped(message: MessageType) {
84-
if (!panel) {
77+
if (!messageHandler.panel) {
8578
console.error("ERROR: panel is not set");
8679
return;
8780
}
88-
sendToFrontend(panel, message);
81+
sendToFrontend(messageHandler.panel, message);
8982
}

src/utils/messageHandler.tsx

Lines changed: 133 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -14,145 +14,149 @@ import { client } from "../extension";
1414
import _ from "lodash";
1515
import { McqPanelWithLogging as McqPanel } from "../webview/components/McqPanel";
1616

17-
let mcqPanel: vscode.WebviewPanel | null = null;
17+
/*
18+
* MessageHandler is a singleton class that handles messages from the frontend
19+
* and manages the state of the webview panels.
20+
* Design choice: Active panel, mcqPanel, and activeEditor are all stored in the same class.
21+
* Trade-off: Requires an instance of MessageHandler to be created and used instead of exporting
22+
* the activeEditor and active panel directly.
23+
*/
24+
export class MessageHandler {
25+
private messageQueue: MessageType[] = [];
26+
private handling = false;
27+
public panel: vscode.WebviewPanel | null = null;
28+
public mcqPanel: vscode.WebviewPanel | null = null;
29+
public activeEditor: Editor | null = null;
1830

19-
let panel: vscode.WebviewPanel | null = null;
20-
// This needs to be a reference to active
21-
// TODO: Fix this ugly code!
22-
export let activeEditor: Editor | null = null;
23-
24-
const messageQueue: MessageType[] = [];
25-
let handling = false;
31+
async handleMessage(context: vscode.ExtensionContext, message: MessageType) {
32+
this.messageQueue.push(message);
33+
if (this.handling) {
34+
return;
35+
}
36+
this.handling = true;
2637

27-
export async function handleMessage(
28-
context: vscode.ExtensionContext,
29-
message: MessageType,
30-
) {
31-
messageQueue.push(message);
32-
if (handling) {
33-
return;
34-
}
35-
handling = true;
38+
while (this.messageQueue.length > 0) {
39+
const message = this.messageQueue.shift()!;
40+
console.log(`${Date.now()} Beginning handleMessage: ${message.type}`);
41+
switch (message.type) {
42+
case MessageTypeNames.ExtensionPing:
43+
sendToFrontend(this.panel, Messages.ExtensionPong(null));
44+
break;
45+
case MessageTypeNames.MCQQuestion: {
46+
if (this.mcqPanel === null) {
47+
this.mcqPanel = vscode.window.createWebviewPanel(
48+
"mcq-question-panel",
49+
`Question ${message.questionId + 1}`,
50+
vscode.ViewColumn.One,
51+
{ enableScripts: true, retainContextWhenHidden: true },
52+
);
3653

37-
while (messageQueue.length > 0) {
38-
const message = messageQueue.shift()!;
39-
console.log(`${Date.now()} Beginning handleMessage: ${message.type}`);
40-
switch (message.type) {
41-
case MessageTypeNames.ExtensionPing:
42-
sendToFrontend(panel, Messages.ExtensionPong(null));
43-
break;
44-
case MessageTypeNames.MCQQuestion: {
45-
if (mcqPanel === null) {
46-
mcqPanel = vscode.window.createWebviewPanel(
47-
"mcq-question-panel",
48-
`Question ${message.questionId + 1}`,
49-
vscode.ViewColumn.One,
50-
{ enableScripts: true, retainContextWhenHidden: true },
54+
this.mcqPanel.onDidDispose(() => {
55+
this.mcqPanel = null;
56+
});
57+
}
58+
this.mcqPanel.title = `Question ${message.questionId + 1}`;
59+
this.mcqPanel.iconPath = vscode.Uri.joinPath(
60+
context.extensionUri,
61+
"assets",
62+
"icon.png",
5163
);
52-
53-
mcqPanel.onDidDispose(() => {
54-
mcqPanel = null;
64+
let activePanel = await McqPanel({
65+
data: {
66+
assessmentName: message.assessmentName ?? "",
67+
questionId: message.questionId,
68+
question: message.question,
69+
choices: message.options,
70+
workspaceLocation: message.workspaceLocation ?? "assessment",
71+
},
5572
});
56-
}
57-
mcqPanel.title = `Question ${message.questionId + 1}`;
58-
mcqPanel.iconPath = vscode.Uri.joinPath(
59-
context.extensionUri,
60-
"assets",
61-
"icon.png",
62-
);
63-
let activePanel = await McqPanel({
64-
data: {
65-
assessmentName: message.assessmentName ?? "",
66-
questionId: message.questionId,
67-
question: message.question,
68-
choices: message.options,
69-
workspaceLocation: message.workspaceLocation ?? "assessment",
70-
},
71-
});
72-
setWebviewContent(
73-
mcqPanel,
74-
context,
75-
<div
76-
// @ts-ignore: Our FakeReact doesn't modify the style attribute
77-
style="width: 100%; height: calc(100vh - 10px)"
78-
id="mcq-panel"
79-
>
80-
{ReactDOMServer.renderToString(activePanel)}
81-
</div>,
82-
);
83-
mcqPanel.reveal(vscode.ViewColumn.One);
73+
setWebviewContent(
74+
this.mcqPanel,
75+
context,
76+
<div
77+
// @ts-ignore: Our FakeReact doesn't modify the style attribute
78+
style="width: 100%; height: calc(100vh - 10px)"
79+
id="mcq-panel"
80+
>
81+
{ReactDOMServer.renderToString(activePanel)}
82+
</div>,
83+
);
84+
this.mcqPanel.reveal(vscode.ViewColumn.One);
8485

85-
break;
86-
}
87-
case MessageTypeNames.MCQAnswer:
88-
console.log(`Received MCQ answer: ${message.choice}`);
89-
sendToFrontend(panel, message);
90-
break;
91-
case MessageTypeNames.NewEditor:
92-
// console.log(message.questionType + " questionType \n");
93-
// if (message.questionType == "mcq") {
94-
// break;
95-
// }
96-
activeEditor = await Editor.create(
97-
message.workspaceLocation,
98-
message.assessmentName,
99-
message.questionId,
100-
message.prepend,
101-
message.initialCode,
102-
);
103-
activeEditor.uri;
104-
const info = context.globalState.get("info") ?? {};
105-
if (activeEditor.uri) {
106-
// TODO: Write our own wrapper to set nested keys easily, removing lodash
107-
// @ts-ignore
108-
_.set(info, `["${activeEditor.uri}"].chapter`, message.chapter ?? 1);
109-
// TODO: message.prepend can be undefined in runtime, investigate
110-
const nPrependLines =
111-
message.prepend && message.prepend !== ""
112-
? message.prepend.split("\n").length + 2 // account for start/end markers
113-
: 0;
114-
_.set(info, `["${activeEditor.uri}"].prepend`, nPrependLines);
115-
context.globalState.update("info", info);
116-
client.sendRequest("source/publishInfo", info);
86+
break;
11787
}
118-
119-
panel?.reveal(vscode.ViewColumn.Two);
120-
console.log(
121-
`EXTENSION: NewEditor: activeEditor set to ${activeEditor.assessmentName}_${activeEditor.questionId}`,
122-
);
123-
124-
activeEditor.onChange((editor) => {
125-
const workspaceLocation = editor.workspaceLocation;
126-
const code = editor.getText();
127-
if (!code) {
128-
return;
129-
}
130-
if (editor !== activeEditor) {
131-
console.log(
132-
`EXTENSION: Editor ${editor.assessmentName}_${editor.questionId}_${editor.assessmentType} is no longer active, skipping onChange`,
88+
case MessageTypeNames.MCQAnswer:
89+
console.log(`Received MCQ answer: ${message.choice}`);
90+
sendToFrontend(this.panel, message);
91+
break;
92+
case MessageTypeNames.NewEditor:
93+
this.activeEditor = await Editor.create(
94+
message.workspaceLocation,
95+
message.assessmentName,
96+
message.questionId,
97+
message.prepend,
98+
message.initialCode,
99+
);
100+
this.activeEditor.uri;
101+
const info = context.globalState.get("info") ?? {};
102+
if (this.activeEditor.uri) {
103+
// TODO: Write our own wrapper to set nested keys easily, removing lodash
104+
// @ts-ignore
105+
_.set(
106+
info,
107+
`["${this.activeEditor.uri}"].chapter`,
108+
message.chapter ?? 1,
133109
);
110+
// TODO: message.prepend can be undefined in runtime, investigate
111+
const nPrependLines =
112+
message.prepend && message.prepend !== ""
113+
? message.prepend.split("\n").length + 2 // account for start/end markers
114+
: 0;
115+
_.set(info, `["${this.activeEditor.uri}"].prepend`, nPrependLines);
116+
context.globalState.update("info", info);
117+
client.sendRequest("source/publishInfo", info);
134118
}
135-
if (activeEditor) {
136-
console.log("activeEditor keys and values:");
137-
Object.entries(activeEditor).forEach(([key, value]) => {
138-
console.log(`${key}:`, value);
139-
});
140-
}
141-
const message = Messages.Text(workspaceLocation, code);
142-
console.log(`Sending message: ${JSON.stringify(message)}`);
143-
sendToFrontend(panel, message);
144-
});
145-
break;
146-
// case MessageTypeNames.Text:
147-
// if (!activeEditor) {
148-
// console.log("ERROR: activeEditor is not set");
149-
// break;
150-
// }
151-
// activeEditor.replace(message.code, "Text");
152-
// break;
119+
120+
this.panel?.reveal(vscode.ViewColumn.Two);
121+
console.log(
122+
`EXTENSION: NewEditor: activeEditor set to ${this.activeEditor.assessmentName}_${this.activeEditor.questionId}`,
123+
);
124+
125+
this.activeEditor.onChange((editor: Editor) => {
126+
const workspaceLocation = editor.workspaceLocation;
127+
const code = editor.getText();
128+
if (!code) {
129+
return;
130+
}
131+
if (editor !== this.activeEditor) {
132+
console.log(
133+
`EXTENSION: Editor ${editor.assessmentName}_${editor.questionId}_${editor.assessmentType} is no longer active, skipping onChange`,
134+
);
135+
}
136+
if (this.activeEditor) {
137+
console.log("activeEditor keys and values:");
138+
Object.entries(this.activeEditor).forEach(([key, value]) => {
139+
console.log(`${key}:`, value);
140+
});
141+
}
142+
const message = Messages.Text(workspaceLocation, code);
143+
console.log(`Sending message: ${JSON.stringify(message)}`);
144+
sendToFrontend(this.panel, message);
145+
});
146+
break;
147+
}
148+
console.log(`${Date.now()} Finish handleMessage: ${message.type}`);
153149
}
154-
console.log(`${Date.now()} Finish handleMessage: ${message.type}`);
150+
151+
this.handling = false;
155152
}
156153

157-
handling = false;
154+
static instance: MessageHandler | null = null;
155+
156+
static getInstance() {
157+
if (!MessageHandler.instance) {
158+
MessageHandler.instance = new MessageHandler();
159+
}
160+
return MessageHandler.instance;
161+
}
158162
}

0 commit comments

Comments
 (0)