Skip to content

Commit f0ce794

Browse files
committed
Merge branch 'main' into chapter-selection
2 parents 86aaf21 + b4d9e56 commit f0ce794

File tree

14 files changed

+580
-191
lines changed

14 files changed

+580
-191
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ If you don't have VS Code installed yet, download it from [here](https://code.vi
2424
From within VS Code,
2525

2626
1. Click on the Extensions icon on the left sidebar or the `View: Show Extensions` command (`Ctrl+Shift+X`).
27-
2. Search for "Source Academy" and install it.
27+
2. Search for "Source Academy" and click "Install". When prompted for `Do you trust the publisher "Source Academy"?` click `Trust Publisher & Install`.
2828

2929
We'd recommend also installing the [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) extension
3030
to show diagnostic messages more prominently.
3131

32+
Source Academy will store your code in the `.sourceacademy` folder of your home directory by default, which you may change in the settings.
33+
If asked, `Do you trust the authors of the files in this folder?`, do click on `Yes, I trust the authors`.
34+
3235
## Getting started
3336

3437
_If you don't have an existing Source Academy account, you can try out our public frontend. See section [Changing the frontend](#changing-the-frontend)._

language-configuration.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
],
2626
"folding": {
2727
"markers": {
28-
"start": "// PREPEND -- DO NOT EDIT",
29-
"end": "// END PREPEND"
28+
"start": "// PREPEND",
29+
"end": "// END PREPEND -- DO NOT EDIT PREPEND"
3030
}
3131
},
3232
"wordPattern": "[_$a-zA-Z][_$a-zA-Z0-9]*",

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 & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -2,126 +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";
11+
import { MessageHandler } from "../utils/messageHandler";
12+
import { SOURCE_ACADEMY_ICON_URI } from "../extension";
1813

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;
26-
27-
// TODO: Remove panel and handling message logic out of the commands/ directory
28-
29-
async function handleMessage(
30-
context: vscode.ExtensionContext,
31-
message: MessageType,
32-
) {
33-
messageQueue.push(message);
34-
if (handling) {
35-
return;
36-
}
37-
handling = true;
38-
39-
while (messageQueue.length > 0) {
40-
const message = messageQueue.shift()!;
41-
console.log(`${Date.now()} Beginning handleMessage: ${message.type}`);
42-
switch (message.type) {
43-
case MessageTypeNames.ExtensionPing:
44-
sendToFrontend(panel, Messages.ExtensionPong(null));
45-
break;
46-
case MessageTypeNames.NewEditor:
47-
activeEditor = await Editor.create(
48-
message.workspaceLocation,
49-
message.assessmentName,
50-
message.questionId,
51-
message.prepend,
52-
message.initialCode,
53-
);
54-
activeEditor.uri;
55-
const info = context.globalState.get("info") ?? {};
56-
if (activeEditor.uri) {
57-
// TODO: Write our own wrapper to set nested keys easily, removing lodash
58-
// @ts-ignore
59-
_.set(info, `["${activeEditor.uri}"].chapter`, message.chapter ?? 1);
60-
// TODO: message.prepend can be undefined in runtime, investigate
61-
const nPrependLines =
62-
message.prepend && message.prepend !== ""
63-
? message.prepend.split("\n").length + 2 // account for start/end markers
64-
: 0;
65-
_.set(info, `["${activeEditor.uri}"].prepend`, nPrependLines);
66-
context.globalState.update("info", info);
67-
client.sendRequest("source/publishInfo", info);
68-
}
69-
70-
panel?.reveal(vscode.ViewColumn.Two);
71-
console.log(
72-
`EXTENSION: NewEditor: activeEditor set to ${activeEditor.assessmentName}_${activeEditor.questionId}`,
73-
);
74-
activeEditor.onChange((editor) => {
75-
const workspaceLocation = editor.workspaceLocation;
76-
const code = editor.getText();
77-
if (!code) {
78-
return;
79-
}
80-
if (editor !== activeEditor) {
81-
console.log(
82-
`EXTENSION: Editor ${editor.assessmentName}_${editor.questionId} is no longer active, skipping onChange`,
83-
);
84-
}
85-
const message = Messages.Text(workspaceLocation, code);
86-
console.log(`Sending message: ${JSON.stringify(message)}`);
87-
sendToFrontend(panel, message);
88-
});
89-
break;
90-
case MessageTypeNames.ChangeChapter: {
91-
const info = context.globalState.get("info") ?? {};
92-
const uri = vscode.Uri.file(Editor.getFilePath(message.assessmentName, message.questionId)).toString();
93-
94-
_.set(info, `["${uri}"].chapter`, message.chapter ?? 1);
95-
context.globalState.update("info", info);
96-
client.sendRequest("source/publishInfo", info);
97-
98-
if (message.variant !== "default") {
99-
vscode.window.showInformationMessage(`The Language Server does not support any variants, the
100-
Language Server will use Source §${message.chapter}, but it is not guaranteed to be accurate.`)
101-
}
102-
103-
104-
break;
105-
}
106-
// case MessageTypeNames.Text:
107-
// if (!activeEditor) {
108-
// console.log("ERROR: activeEditor is not set");
109-
// break;
110-
// }
111-
// activeEditor.replace(message.code, "Text");
112-
// break;
113-
case MessageTypeNames.NotifyAssessmentsOverview:
114-
const { assessmentOverviews, courseId } = message;
115-
context.globalState.update("assessmentOverviews", assessmentOverviews);
116-
context.globalState.update("courseId", courseId);
117-
treeDataProvider.refresh();
118-
break;
119-
}
120-
console.log(`${Date.now()} Finish handleMessage: ${message.type}`);
121-
}
122-
123-
handling = false;
124-
}
14+
let messageHandler = MessageHandler.getInstance();
12515

12616
export async function showPanel(
12717
context: vscode.ExtensionContext,
@@ -135,7 +25,7 @@ export async function showPanel(
13525
// Get a reference to the active editor (before the focus is switched to our newly created webview)
13626
// firstEditor = vscode.window.activeTextEditor!;
13727

138-
panel = vscode.window.createWebviewPanel(
28+
messageHandler.panel = vscode.window.createWebviewPanel(
13929
"source-academy-panel",
14030
"Source Academy",
14131
vscode.ViewColumn.Beside,
@@ -145,18 +35,17 @@ export async function showPanel(
14535
},
14636
);
14737

148-
panel.webview.onDidReceiveMessage(
149-
(message: MessageType) => handleMessage(context, message),
38+
messageHandler.panel.webview.onDidReceiveMessage(
39+
(message: MessageType) => messageHandler.handleMessage(context, message),
15040
undefined,
15141
context.subscriptions,
15242
);
15343

15444
const iframeUrl = new URL(route ?? "/playground", config.frontendBaseUrl)
15545
.href;
15646

157-
// equivalent to panel.webview.html = ...
15847
setWebviewContent(
159-
panel,
48+
messageHandler.panel,
16049
context,
16150
// NOTE: This is not React code, but our FakeReact!
16251
<div
@@ -176,16 +65,16 @@ export async function showPanel(
17665
</div>,
17766
);
17867

179-
panel.iconPath = SOURCE_ACADEMY_ICON_URI;
68+
messageHandler.panel.iconPath = SOURCE_ACADEMY_ICON_URI;
18069
}
18170

182-
// TODO: Move this to a util file
18371
export async function sendToFrontendWrapped(message: MessageType) {
72+
sendToFrontend(messageHandler.panel, message);
18473
// TODO: This returning of status code shouldn't be necessary after refactor
185-
if (!panel) {
74+
if (!messageHandler.panel) {
18675
console.error("ERROR: panel is not set");
18776
return false;
18877
}
189-
sendToFrontend(panel, message);
78+
sendToFrontend(messageHandler.panel, message);
19079
return true;
19180
}

src/utils/editor.ts

Lines changed: 50 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
// TODO: Move this file to src/features/editor/editor.ts
12
import * as vscode from "vscode";
23

34
import config from "../utils/config";
45
import Messages, { VscWorkspaceLocation } from "./messages";
56
import path from "path";
67
import { sendToFrontendWrapped } from "../commands/showPanel";
78
import { canonicaliseLocation } from "./misc";
9+
import { codeAddPrepend, codeRemovePrepend } from "./editorUtils";
810

911
export class Editor {
1012
editor?: vscode.TextEditor;
1113
workspaceLocation: VscWorkspaceLocation;
1214
assessmentName: string;
1315
questionId: number;
16+
assessmentType: string | null = null;
1417
onChangeCallback?: (editor: Editor) => void;
1518
code: string | null = null;
1619
uri: string | null = null;
@@ -26,6 +29,7 @@ export class Editor {
2629
) {
2730
this.workspaceLocation = workspaceLocation;
2831
this.assessmentName = assessmentName;
32+
this.assessmentType = this.assessmentType;
2933
this.questionId = questionId;
3034
}
3135

@@ -64,58 +68,53 @@ export class Editor {
6468
const uri = vscode.Uri.file(filePath);
6569
self.uri = uri.toString();
6670

67-
const contents =
68-
prepend !== ""
69-
? [
70-
"// PREPEND -- DO NOT EDIT",
71-
prepend,
72-
"// END PREPEND",
73-
initialCode,
74-
].join("\n")
75-
: initialCode;
76-
77-
await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)).then(
78-
(value) => {
79-
if (value.toString() !== contents) {
80-
self.log(
81-
"EXTENSION: Conflict detected between local and remote, prompting user to choose one",
71+
const contents = codeAddPrepend(initialCode, prepend);
72+
73+
await vscode.workspace.fs
74+
.readFile(vscode.Uri.file(filePath))
75+
.then((value) => value.toString())
76+
.then(
77+
(localCode) => {
78+
if (localCode !== contents) {
79+
self.log(
80+
"EXTENSION: Conflict detected between local and remote, prompting user to choose one",
81+
);
82+
vscode.window
83+
.showInformationMessage(
84+
[
85+
"The local file differs from the version on the Source Academy servers.",
86+
"Discard the local file and use the one on the server?",
87+
].join(" "),
88+
{ modal: true },
89+
"Yes",
90+
)
91+
.then(async (answer) => {
92+
// By default the code displayed is the local one
93+
if (answer === "Yes") {
94+
self.log("EXTENSION: Saving program from server to file");
95+
await vscode.workspace.fs.writeFile(
96+
uri,
97+
new TextEncoder().encode(contents),
98+
);
99+
} else if (answer === undefined) {
100+
// Modal cancelled
101+
const message = Messages.Text(
102+
self.workspaceLocation,
103+
codeRemovePrepend(localCode),
104+
);
105+
sendToFrontendWrapped(message);
106+
}
107+
});
108+
}
109+
},
110+
async () => {
111+
self.log(`Opening file failed, creating at ${filePath}`);
112+
await vscode.workspace.fs.writeFile(
113+
uri,
114+
new TextEncoder().encode(contents),
82115
);
83-
vscode.window
84-
.showInformationMessage(
85-
[
86-
"The local file differs from the version on the Source Academy servers.",
87-
"Discard the local file and use the one on the server?",
88-
].join(" "),
89-
{ modal: true },
90-
"Yes",
91-
)
92-
.then(async (answer) => {
93-
// By default the code displayed is the local one
94-
if (answer === "Yes") {
95-
self.log("EXTENSION: Saving program from server to file");
96-
await vscode.workspace.fs.writeFile(
97-
uri,
98-
new TextEncoder().encode(contents),
99-
);
100-
} else if (answer === undefined) {
101-
// Modal cancelled
102-
const message = Messages.Text(
103-
self.workspaceLocation,
104-
value.toString(),
105-
);
106-
sendToFrontendWrapped(message);
107-
}
108-
});
109-
}
110-
},
111-
async () => {
112-
self.log(`Opening file failed, creating at ${filePath}`);
113-
await vscode.workspace.fs.writeFile(
114-
uri,
115-
new TextEncoder().encode(contents),
116-
);
117-
},
118-
);
116+
},
117+
);
119118

120119
const editor = await vscode.window.showTextDocument(uri, {
121120
preview: false,

0 commit comments

Comments
 (0)