Skip to content

Commit 5cadc73

Browse files
committed
Merge branch 'main' into fix-prepend
2 parents 57e986f + 538a13b commit 5cadc73

File tree

11 files changed

+464
-119
lines changed

11 files changed

+464
-119
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"@types/lodash": "^4.17.9",
132132
"@types/node": "^22.5.4",
133133
"@types/react": "^19.0.12",
134+
"@types/showdown": "^2.0.6",
134135
"@types/vscode": "^1.93.0",
135136
"@vscode/vsce": "^3.2.2",
136137
"esbuild": "^0.25.0",
@@ -148,6 +149,7 @@
148149
"lodash": "^4.17.21",
149150
"react": "^19.0.0",
150151
"react-dom": "^19.0.0",
152+
"showdown": "^2.1.0",
151153
"vscode-languageclient": "^9.0.1"
152154
},
153155
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"

scripts/lsp.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function main() {
2222

2323
async function downloadLsp() {
2424
const outputFolder = getOutputDir();
25-
const version = "0.1.8";
25+
const version = "0.1.9";
2626
const lspFilename = "source-lsp.js";
2727
const url = `https://github.com/source-academy/source-lsp/releases/download/v${version}/${lspFilename}`;
2828
// const url = `https://github.com/source-academy/source-lsp/releases/latest/download/${lspFilename}`;

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: 12 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,108 +2,16 @@ import * as vscode from "vscode";
22
// Allow using JSX within this file by overriding our own createElement function
33
import React from "../utils/FakeReact";
44

5-
import Messages, {
6-
MessageType,
7-
MessageTypeNames,
8-
sendToFrontend,
9-
} from "../utils/messages";
5+
import { MessageType, sendToFrontend } from "../utils/messages";
106
import { LANGUAGES } from "../utils/languages";
117
import { setWebviewContent } from "../utils/webview";
128
import config from "../utils/config";
13-
import { Editor } from "../utils/editor";
149
import { FRONTEND_ELEMENT_ID } from "../constants";
15-
import { client, SOURCE_ACADEMY_ICON_URI } from "../extension";
1610
import _ from "lodash";
17-
import { treeDataProvider } from "../treeview";
18-
import { codeRemovePrepend, getNumPrependLines } from "../utils/editorUtils";
11+
import { MessageHandler } from "../utils/messageHandler";
12+
import { SOURCE_ACADEMY_ICON_URI } from "../extension";
1913

20-
let panel: vscode.WebviewPanel | null = null;
21-
// This needs to be a reference to active
22-
// TODO: Fix this ugly code!
23-
export let activeEditor: Editor | null = null;
24-
25-
const messageQueue: MessageType[] = [];
26-
let handling = false;
27-
28-
// TODO: Remove panel and handling message logic out of the commands/ directory
29-
30-
async function handleMessage(
31-
context: vscode.ExtensionContext,
32-
message: MessageType,
33-
) {
34-
messageQueue.push(message);
35-
if (handling) {
36-
return;
37-
}
38-
handling = true;
39-
40-
while (messageQueue.length > 0) {
41-
const message = messageQueue.shift()!;
42-
console.log(`${Date.now()} Beginning handleMessage: ${message.type}`);
43-
switch (message.type) {
44-
case MessageTypeNames.ExtensionPing:
45-
sendToFrontend(panel, Messages.ExtensionPong(null));
46-
break;
47-
case MessageTypeNames.NewEditor:
48-
activeEditor = await Editor.create(
49-
message.workspaceLocation,
50-
message.assessmentName,
51-
message.questionId,
52-
message.prepend,
53-
message.initialCode,
54-
);
55-
activeEditor.uri;
56-
const info = context.globalState.get("info") ?? {};
57-
if (activeEditor.uri) {
58-
// TODO: Write our own wrapper to set nested keys easily, removing lodash
59-
_.set(info, `["${activeEditor.uri}"].chapter`, message.chapter ?? 1);
60-
const nPrependLines = getNumPrependLines(message.prepend);
61-
_.set(info, `["${activeEditor.uri}"].prepend`, nPrependLines);
62-
context.globalState.update("info", info);
63-
client.sendRequest("source/publishInfo", info);
64-
}
65-
66-
panel?.reveal(vscode.ViewColumn.Two);
67-
console.log(
68-
`EXTENSION: NewEditor: activeEditor set to ${activeEditor.assessmentName}_${activeEditor.questionId}`,
69-
);
70-
activeEditor.onChange((editor) => {
71-
const workspaceLocation = editor.workspaceLocation;
72-
const code = editor.getText();
73-
if (!code) {
74-
return;
75-
}
76-
if (editor !== activeEditor) {
77-
console.log(
78-
`EXTENSION: Editor ${editor.assessmentName}_${editor.questionId} is no longer active, skipping onChange`,
79-
);
80-
}
81-
const message = Messages.Text(
82-
workspaceLocation,
83-
codeRemovePrepend(code),
84-
);
85-
sendToFrontend(panel, message);
86-
});
87-
break;
88-
// case MessageTypeNames.Text:
89-
// if (!activeEditor) {
90-
// console.log("ERROR: activeEditor is not set");
91-
// break;
92-
// }
93-
// activeEditor.replace(message.code, "Text");
94-
// break;
95-
case MessageTypeNames.NotifyAssessmentsOverview:
96-
const { assessmentOverviews, courseId } = message;
97-
context.globalState.update("assessmentOverviews", assessmentOverviews);
98-
context.globalState.update("courseId", courseId);
99-
treeDataProvider.refresh();
100-
break;
101-
}
102-
console.log(`${Date.now()} Finish handleMessage: ${message.type}`);
103-
}
104-
105-
handling = false;
106-
}
14+
let messageHandler = MessageHandler.getInstance();
10715

10816
export async function showPanel(
10917
context: vscode.ExtensionContext,
@@ -117,7 +25,7 @@ export async function showPanel(
11725
// Get a reference to the active editor (before the focus is switched to our newly created webview)
11826
// firstEditor = vscode.window.activeTextEditor!;
11927

120-
panel = vscode.window.createWebviewPanel(
28+
messageHandler.panel = vscode.window.createWebviewPanel(
12129
"source-academy-panel",
12230
"Source Academy",
12331
vscode.ViewColumn.Beside,
@@ -127,18 +35,17 @@ export async function showPanel(
12735
},
12836
);
12937

130-
panel.webview.onDidReceiveMessage(
131-
(message: MessageType) => handleMessage(context, message),
38+
messageHandler.panel.webview.onDidReceiveMessage(
39+
(message: MessageType) => messageHandler.handleMessage(context, message),
13240
undefined,
13341
context.subscriptions,
13442
);
13543

13644
const iframeUrl = new URL(route ?? "/playground", config.frontendBaseUrl)
13745
.href;
13846

139-
// equivalent to panel.webview.html = ...
14047
setWebviewContent(
141-
panel,
48+
messageHandler.panel,
14249
context,
14350
// NOTE: This is not React code, but our FakeReact!
14451
<div
@@ -158,16 +65,16 @@ export async function showPanel(
15865
</div>,
15966
);
16067

161-
panel.iconPath = SOURCE_ACADEMY_ICON_URI;
68+
messageHandler.panel.iconPath = SOURCE_ACADEMY_ICON_URI;
16269
}
16370

164-
// TODO: Move this to a util file
16571
export async function sendToFrontendWrapped(message: MessageType) {
72+
sendToFrontend(messageHandler.panel, message);
16673
// TODO: This returning of status code shouldn't be necessary after refactor
167-
if (!panel) {
74+
if (!messageHandler.panel) {
16875
console.error("ERROR: panel is not set");
16976
return false;
17077
}
171-
sendToFrontend(panel, message);
78+
sendToFrontend(messageHandler.panel, message);
17279
return true;
17380
}

src/utils/editor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class Editor {
1313
workspaceLocation: VscWorkspaceLocation;
1414
assessmentName: string;
1515
questionId: number;
16+
assessmentType: string | null = null;
1617
onChangeCallback?: (editor: Editor) => void;
1718
code: string | null = null;
1819
uri: string | null = null;
@@ -28,6 +29,7 @@ export class Editor {
2829
) {
2930
this.workspaceLocation = workspaceLocation;
3031
this.assessmentName = assessmentName;
32+
this.assessmentType = this.assessmentType;
3133
this.questionId = questionId;
3234
}
3335

src/utils/markdown.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Lightweight Markdown → HTML helper for the VS Code Webview.
2+
// Uses `showdown` (≈12 kB gzipped, no React peer-deps) for robust CommonMark
3+
// conversion
4+
// Keeps dependency surface minimal (one package)
5+
6+
import { Converter } from "showdown";
7+
8+
// A singleton converter instance (Showdown initialisation is expensive)
9+
const converter = new Converter({
10+
tables: true,
11+
simplifiedAutoLink: true,
12+
strikethrough: true,
13+
tasklists: true,
14+
openLinksInNewWindow: true,
15+
});
16+
17+
// Very small sanitiser: strips <script> tags and dangerous inline event attrs.
18+
const sanitize = (html: string): string => {
19+
// Remove script/style tags completely
20+
let safe = html.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, "");
21+
// Remove on*="..." inline event handlers
22+
safe = safe.replace(/ on\w+="[^"]*"/g, "");
23+
return safe;
24+
};
25+
26+
export function markdownToHtml(markdown: string): string {
27+
const rawHtml = converter.makeHtml(markdown);
28+
return sanitize(rawHtml);
29+
}
30+
31+
export default markdownToHtml;

0 commit comments

Comments
 (0)