diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 62a05d0..73ffbf4 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -8,38 +8,46 @@ import { sendToFrontendWrapped } from "../commands/showPanel"; import { canonicaliseLocation } from "./misc"; import { codeAddPrepend, codeRemovePrepend } from "./editorUtils"; +/** + * Represents a VS Code editor associated with a Source Academy question. + * Abstracts low level calling of VS Code APIs. + */ export class Editor { - editor?: vscode.TextEditor; + private editor: vscode.TextEditor; + private onChangeCallback?: (editor: Editor) => void; + + // Data associated with TextEditor + prepend: string; + uri: string; + + // Metadata relating to this question workspaceLocation: VscWorkspaceLocation; assessmentName: string; questionId: number; - assessmentType: string | null = null; - onChangeCallback?: (editor: Editor) => void; - code: string | null = null; - uri: string | null = null; - // For debugging purposes - replaceTime: number = 0; - nBursty: number = 0; - - constructor( + private constructor( + editor: vscode.TextEditor, + prepend: string, + uri: string, workspaceLocation: VscWorkspaceLocation, assessmentName: string, questionId: number, ) { + this.editor = editor; + this.prepend = prepend; + this.uri = uri; this.workspaceLocation = workspaceLocation; this.assessmentName = assessmentName; - this.assessmentType = this.assessmentType; this.questionId = questionId; } /** For debugging purposes */ - log(text: string) { - console.log(`${this.editor?.document.fileName.split("/").at(-1)} ${text}`); + private log(text: string) { + console.log(`${this.editor.document.fileName.split("/").at(-1)} ${text}`); } getText() { - return this.editor?.document.getText(); + return this.editor.document.getText(); } // TODO: This method is too loaded, it's not obvious it also shows the editor @@ -50,19 +58,12 @@ export class Editor { prepend: string = "", initialCode: string = "", ): Promise { - const self = new Editor(workspaceLocation, assessmentName, questionId); - self.assessmentName = assessmentName; - self.questionId = questionId; - const workspaceFolder = canonicaliseLocation(config.workspaceFolder); - const filePath = path.join( workspaceFolder, `${assessmentName}_${questionId}.js`, ); - const uri = vscode.Uri.file(filePath); - self.uri = uri.toString(); const contents = codeAddPrepend(initialCode, prepend); @@ -72,9 +73,6 @@ export class Editor { .then( (localCode) => { if (localCode !== contents) { - self.log( - "EXTENSION: Conflict detected between local and remote, prompting user to choose one", - ); vscode.window .showInformationMessage( [ @@ -87,7 +85,6 @@ export class Editor { .then(async (answer) => { // By default the code displayed is the local one if (answer === "Yes") { - self.log("EXTENSION: Saving program from server to file"); await vscode.workspace.fs.writeFile( uri, new TextEncoder().encode(contents), @@ -95,7 +92,7 @@ export class Editor { } else if (answer === undefined) { // Modal cancelled const message = Messages.Text( - self.workspaceLocation, + workspaceLocation, codeRemovePrepend(localCode), ); sendToFrontendWrapped(message); @@ -104,7 +101,6 @@ export class Editor { } }, async () => { - self.log(`Opening file failed, creating at ${filePath}`); await vscode.workspace.fs.writeFile( uri, new TextEncoder().encode(contents), @@ -116,79 +112,58 @@ export class Editor { preview: false, viewColumn: vscode.ViewColumn.One, }); + + // Programmatically set the language vscode.languages.setTextDocumentLanguage(editor.document, "source"); + + // Collapse the prepend section editor.selection = new vscode.Selection( editor.document.positionAt(0), editor.document.positionAt(1), ); vscode.commands.executeCommand("editor.fold"); - self.editor = editor; + // Create wrapper + const self = new Editor( + editor, + prepend, + uri.toString(), + workspaceLocation, + assessmentName, + questionId, + ); + + // Register callback when contents changed vscode.workspace.onDidChangeTextDocument( (e: vscode.TextDocumentChangeEvent) => { if (!self.onChangeCallback) { return; } - const text = editor.document.getText(); if (e.contentChanges.length === 0) { self.log(`EXTENSION: Editor's code did not change, ignoring`); return; } - if (Date.now() - self.replaceTime < 1000) { - self.log( - `EXTENSION: Ignoring change event, ${Date.now() - self.replaceTime}ms since last replace`, - ); - return; - } - self.log(`EXTENSION: Editor's code changed!`); self.onChangeCallback(self); - self.code = text; }, ); + return self; } - async replace(code: string, tag: string = "") { - if (!this.editor) { - return; - } - this.log(`EXTENSION: Editor's replace called by ${tag}: <<${code}>>`); - if (this.nBursty > 5) { - if (Date.now() - this.replaceTime < 5000) { - this.log(`EXTENSION: TOO BURSTY`); - return; - } - this.nBursty = 0; - } - if (Date.now() - this.replaceTime < 1000) { - this.nBursty++; - } - // this.disableCallback = true; + async replace(code: string) { const editor = this.editor; - // Don't replace if the code is the same + const contents = codeAddPrepend(code, this.prepend); + + // In some sense, simulate a select all and paste editor.edit((editBuilder) => { editBuilder.replace( new vscode.Range( editor.document.positionAt(0), editor.document.positionAt(editor.document.getText().length), ), - code, + contents, ); }); - let retry = 0; - while (editor.document.getText() !== code) { - await new Promise((r) => setTimeout(r, 100)); - this.log( - `EXTENSION: Editor's not replace yet, lets wait: ${editor.document.getText()}`, - ); - retry++; - if (retry > 11) { - this.log(`EXTENSION: Editor's replace wait limit reached`); - break; - } - } - this.code = code; - this.replaceTime = Date.now(); } onChange( diff --git a/src/utils/messageHandler.tsx b/src/utils/messageHandler.tsx index c17415d..ca696cb 100644 --- a/src/utils/messageHandler.tsx +++ b/src/utils/messageHandler.tsx @@ -147,7 +147,7 @@ export class MessageHandler { } if (editor !== this.activeEditor) { console.log( - `EXTENSION: Editor ${editor.assessmentName}_${editor.questionId}_${editor.assessmentType} is no longer active, skipping onChange`, + `EXTENSION: Editor ${editor.assessmentName}_${editor.questionId} is no longer active, skipping onChange`, ); } const message = Messages.Text( @@ -167,6 +167,12 @@ export class MessageHandler { context.globalState.update("courseId", courseId); treeDataProvider.refresh(); break; + case MessageTypeNames.ResetEditor: + if (this.activeEditor) { + this.activeEditor.replace(message.initialCode); + this.panel?.reveal(vscode.ViewColumn.Two); + } + break; } console.log(`${Date.now()} Finish handleMessage: ${message.type}`); } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index b35c498..0ce9367 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -48,6 +48,13 @@ const Messages = createMessages({ EvalEditor: (workspaceLocation: VscWorkspaceLocation) => ({ workspaceLocation: workspaceLocation, }), + ResetEditor: ( + workspaceLocation: VscWorkspaceLocation, + initialCode: string, + ) => ({ + workspaceLocation, + initialCode, + }), NotifyAssessmentsOverview: ( assessmentOverviews: VscAssessmentOverview[], courseId: number,