[extension] More cleanup

This commit is contained in:
orosmatthew 2023-10-16 13:11:54 -04:00
parent 88be84bb62
commit 6fb6f57ece
4 changed files with 284 additions and 316 deletions

View File

@ -4,6 +4,20 @@ import { cloneAndOpenRepo } from './extension';
import { BWPanel } from './problemPanel'; import { BWPanel } from './problemPanel';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
export type TeamData = {
teamId: number;
contestId: number;
};
export type WebviewMessageType = { msg: 'onLogin'; data: TeamData } | { msg: 'onLogout' };
export type MessageType =
| { msg: 'onTestAndSubmit' }
| { msg: 'onStartup' }
| { msg: 'onClone'; data: { contestId: number; teamId: number } }
| { msg: 'onLogin'; data: { teamName: string; password: string } }
| { msg: 'onLogout' };
export class SidebarProvider implements vscode.WebviewViewProvider { export class SidebarProvider implements vscode.WebviewViewProvider {
constructor( constructor(
private readonly extensionUri: vscode.Uri, private readonly extensionUri: vscode.Uri,
@ -19,43 +33,40 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}; };
webview.html = this.getHtmlForWebview(webview); webview.html = this.getHtmlForWebview(webview);
webview.onDidReceiveMessage(async (data: { type: string; value: any }) => { const webviewPostMessage = (m: WebviewMessageType) => {
switch (data.type) { webview.postMessage(m);
};
webview.onDidReceiveMessage(async (m: MessageType) => {
switch (m.msg) {
case 'onTestAndSubmit': { case 'onTestAndSubmit': {
if (this.context) { if (this.context) {
BWPanel.createOrShow(this.context, this.webUrl); BWPanel.show(this.context, this.webUrl);
} }
break; break;
} }
case 'onStartup': { case 'onStartup': {
const token: string | undefined = this.context.globalState.get('token'); const token: string | undefined = this.context.globalState.get('token');
const teamData = this.context.globalState.get('teamData'); const teamData = this.context.globalState.get('teamData') as TeamData | undefined;
if (token && teamData !== undefined) { if (token && teamData !== undefined) {
webview.postMessage({ webviewPostMessage({
type: 'onLogin', msg: 'onLogin',
value: teamData data: teamData
}); });
} }
webview.postMessage({
type: 'onWebUrl',
value: this.webUrl
});
break; break;
} }
case 'onClone': { case 'onClone': {
if (!data.value || !data.value.contestId || !data.value.teamId) { await cloneAndOpenRepo(m.data.contestId, m.data.teamId);
return;
}
await cloneAndOpenRepo(parseInt(data.value.contestId), parseInt(data.value.teamId));
break; break;
} }
case 'requestLogin': { case 'onLogin': {
const res = await fetch(urlJoin(this.webUrl, '/api/team/login'), { const res = await fetch(urlJoin(this.webUrl, '/api/team/login'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
teamname: data.value.teamname, teamname: m.data.teamName,
password: data.value.password password: m.data.password
}) })
}); });
const thing = await res.json(); const thing = await res.json();
@ -72,13 +83,13 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
return; return;
} }
this.context.globalState.update('teamData', data2.data); this.context.globalState.update('teamData', data2.data);
webview.postMessage({ type: 'onLogin', value: data2.data }); webviewPostMessage({ msg: 'onLogout' });
break; break;
} }
case 'requestLogout': { case 'onLogout': {
const sessionToken = this.context.globalState.get<string>('token'); const sessionToken = this.context.globalState.get<string>('token');
if (sessionToken === undefined) { if (sessionToken === undefined) {
webview.postMessage({ type: 'onLogout' }); webviewPostMessage({ msg: 'onLogout' });
} }
const res = await fetch(urlJoin(this.webUrl, '/api/team/logout'), { const res = await fetch(urlJoin(this.webUrl, '/api/team/logout'), {
method: 'POST', method: 'POST',
@ -92,29 +103,29 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
} }
const data2 = await res.json(); const data2 = await res.json();
if (data2.success === true) { if (data2.success === true) {
webview.postMessage({ type: 'onLogout' }); webviewPostMessage({ msg: 'onLogout' });
this.context.globalState.update('token', undefined); this.context.globalState.update('token', undefined);
} }
break; break;
} }
case 'onLogout': { // case 'onLogout': {
this.context.globalState.update('token', null); // this.context.globalState.update('token', null);
break; // break;
} // }
case 'onInfo': { // case 'onInfo': {
if (!data.value) { // if (!data.value) {
return; // return;
} // }
vscode.window.showInformationMessage(data.value); // vscode.window.showInformationMessage(data.value);
break; // break;
} // }
case 'onError': { // case 'onError': {
if (!data.value) { // if (!data.value) {
return; // return;
} // }
vscode.window.showErrorMessage(data.value); // vscode.window.showErrorMessage(data.value);
break; // break;
} // }
} }
}); });
} }

View File

@ -4,46 +4,56 @@ import { runJava } from './run/java';
import { extensionSettings } from './extension'; import { extensionSettings } from './extension';
import { join } from 'path'; import { join } from 'path';
import { submitProblem } from './submit'; import { submitProblem } from './submit';
import urlJoin from 'url-join';
export class BWPanel { export type ProblemData = {
/** id: number;
* Track the currently panel. Only allow a single panel to exist at a time. name: string;
pascalName: string;
sampleInput: string;
sampleOutput: string;
}[];
export type MessageType = { msg: 'onRequestProblemData' };
export type WebviewMessageType = { msg: 'onProblemData'; data: ProblemData };
/**
* Singleton class for problem panel
*/ */
export class BWPanel {
public static currentPanel: BWPanel | undefined; public static currentPanel: BWPanel | undefined;
public static readonly viewType = 'bwpanel'; private running: boolean = false;
private kill: Function | null = null;
private readonly _panel: vscode.WebviewPanel; private constructor(
private readonly _extensionUri: vscode.Uri; private readonly context: vscode.ExtensionContext,
private _disposables: vscode.Disposable[] = []; private readonly panel: vscode.WebviewPanel,
private static _context?: vscode.ExtensionContext; private readonly extensionUri: vscode.Uri,
private static _running: boolean; private readonly webUrl: string
private static _kill: Function | null; ) {
this.update();
}
public static createOrShow(context: vscode.ExtensionContext, webUrl: string) { public static show(context: vscode.ExtensionContext, webUrl: string) {
this._context = context;
const column = vscode.window.activeTextEditor const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn ? vscode.window.activeTextEditor.viewColumn
: undefined; : undefined;
// If we already have a panel, show it. // Show panel if exists
if (BWPanel.currentPanel) { if (BWPanel.currentPanel) {
BWPanel.currentPanel._panel.reveal(column); BWPanel.currentPanel.panel.reveal(column);
return; return;
} }
// Otherwise, create a new panel. // Otherwise create new panel
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
BWPanel.viewType, 'bwpanel',
'BWContest', 'BWContest',
column || vscode.ViewColumn.One, column || vscode.ViewColumn.One,
{ {
// Enable javascript in the webview
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
// And restrict the webview to only loading content from our extension's `media` directory.
localResourceRoots: [ localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, 'media'), vscode.Uri.joinPath(context.extensionUri, 'media'),
vscode.Uri.joinPath(context.extensionUri, 'out/compiled') vscode.Uri.joinPath(context.extensionUri, 'out/compiled')
@ -51,7 +61,7 @@ export class BWPanel {
} }
); );
BWPanel.currentPanel = new BWPanel(panel, context.extensionUri, webUrl); BWPanel.currentPanel = new BWPanel(context, panel, context.extensionUri, webUrl);
} }
public static kill() { public static kill() {
@ -59,149 +69,134 @@ export class BWPanel {
BWPanel.currentPanel = undefined; BWPanel.currentPanel = undefined;
} }
public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, webUrl: string) {
BWPanel.currentPanel = new BWPanel(panel, extensionUri, webUrl);
}
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, webUrl: string) {
this._panel = panel;
this._extensionUri = extensionUri;
this._update();
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
}
public dispose() { public dispose() {
this.panel.dispose();
BWPanel.currentPanel = undefined; BWPanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
const x = this._disposables.pop();
if (x) {
x.dispose();
}
}
} }
private async _update() { private webviewPostMessage(m: WebviewMessageType) {
const webview = this._panel.webview; this.panel.webview.postMessage(m);
}
this._panel.webview.html = this._getHtmlForWebview(webview); private async update() {
webview.onDidReceiveMessage(async (data) => { const webview = this.panel.webview;
switch (data.type) {
case 'onKill': {
if (!BWPanel._running || !BWPanel._kill) {
break;
}
BWPanel._kill();
}
case 'onSubmit': {
await vscode.workspace.saveAll();
if (!data.value) {
return;
}
const ans = await vscode.window.showInformationMessage(
`Are you sure you want to submit ${data.value.problemName}?`,
'Yes',
'No'
);
if (ans !== 'Yes') {
break;
}
try {
await submitProblem(
data.value.sessionToken,
data.value.contestId,
data.value.teamId,
data.value.problemId
);
} catch (err: any) {
vscode.window.showErrorMessage(err.message ?? 'Submission unsuccessful');
break;
}
vscode.window.showInformationMessage('Submitted!');
break;
}
case 'onRun': {
if (BWPanel._running === true) {
vscode.window.showErrorMessage('Already running');
break;
}
await vscode.workspace.saveAll();
if (!data.value) {
break;
}
const repoDir = extensionSettings().repoClonePath;
BWPanel._running = true;
const process = await runJava(
join(
repoDir,
'BWContest',
data.value.contestId.toString(),
data.value.teamId.toString(),
data.value.problemPascalName.toString()
),
join(
repoDir,
'BWContest',
data.value.contestId.toString(),
data.value.teamId.toString(),
data.value.problemPascalName.toString(),
`${data.value.problemPascalName}.java`
),
data.value.problemPascalName,
data.value.input
);
if (!process) {
this._panel.webview.postMessage({
type: 'onOutput',
value: '[An error occurred while running]'
});
break;
}
process.output
.then((output) => {
this._panel.webview.postMessage({ type: 'onOutput', value: output });
BWPanel._running = false;
BWPanel._kill = null;
})
.catch(() => {
this._panel.webview.postMessage({
type: 'onOutput',
value: '[An error occurred while running]'
});
BWPanel._running = false;
BWPanel._kill = null;
});
BWPanel._kill = process.kill;
break;
}
case 'onStartup': {
const token: string | undefined = BWPanel._context?.globalState.get('token');
if (token) { this.panel.webview.html = this._getHtmlForWebview(webview);
this._panel.webview.postMessage({ webview.onDidReceiveMessage(async (m: MessageType) => {
type: 'onSession', switch (m.msg) {
value: token // case 'onKill': {
// if (!this.running || !this.kill) {
// break;
// }
// this.kill();
// }
// case 'onSubmit': {
// await vscode.workspace.saveAll();
// if (!data.value) {
// return;
// }
// const ans = await vscode.window.showInformationMessage(
// `Are you sure you want to submit ${data.value.problemName}?`,
// 'Yes',
// 'No'
// );
// if (ans !== 'Yes') {
// break;
// }
// try {
// await submitProblem(
// data.value.sessionToken,
// data.value.contestId,
// data.value.teamId,
// data.value.problemId
// );
// } catch (err: any) {
// vscode.window.showErrorMessage(err.message ?? 'Submission unsuccessful');
// break;
// }
// vscode.window.showInformationMessage('Submitted!');
// break;
// }
// case 'onRun': {
// if (this.running === true) {
// vscode.window.showErrorMessage('Already running');
// break;
// }
// await vscode.workspace.saveAll();
// if (!data.value) {
// break;
// }
// const repoDir = extensionSettings().repoClonePath;
// this.running = true;
// const process = await runJava(
// join(
// repoDir,
// 'BWContest',
// data.value.contestId.toString(),
// data.value.teamId.toString(),
// data.value.problemPascalName.toString()
// ),
// join(
// repoDir,
// 'BWContest',
// data.value.contestId.toString(),
// data.value.teamId.toString(),
// data.value.problemPascalName.toString(),
// `${data.value.problemPascalName}.java`
// ),
// data.value.problemPascalName,
// data.value.input
// );
// if (!process) {
// this.panel.webview.postMessage({
// type: 'onOutput',
// value: '[An error occurred while running]'
// });
// break;
// }
// process.output
// .then((output) => {
// this.panel.webview.postMessage({ type: 'onOutput', value: output });
// this.running = false;
// this.kill = null;
// })
// .catch(() => {
// this.panel.webview.postMessage({
// type: 'onOutput',
// value: '[An error occurred while running]'
// });
// this.running = false;
// this.kill = null;
// });
// this.kill = process.kill;
// break;
// }
case 'onRequestProblemData': {
const token: string | undefined = this.context.globalState.get('token');
if (token !== undefined) {
const res = await fetch(urlJoin(this.webUrl, `/api/contest/${token}`));
const data = await res.json();
if (data.success === true) {
this.webviewPostMessage({
msg: 'onProblemData',
data: data.problems
}); });
} }
}
break; break;
} }
case 'onInfo': { // case 'onInfo': {
if (!data.value) { // if (!data.value) {
return; // return;
} // }
vscode.window.showInformationMessage(data.value); // vscode.window.showInformationMessage(data.value);
break; // break;
} // }
case 'onError': { // case 'onError': {
if (!data.value) { // if (!data.value) {
return; // return;
} // }
vscode.window.showErrorMessage(data.value); // vscode.window.showErrorMessage(data.value);
break;
}
// case "tokens": {
// await Util.globalState.update(accessTokenKey, data.accessToken);
// await Util.globalState.update(refreshTokenKey, data.refreshToken);
// break; // break;
// } // }
} }
@ -209,23 +204,20 @@ export class BWPanel {
} }
private _getHtmlForWebview(webview: vscode.Webview) { private _getHtmlForWebview(webview: vscode.Webview) {
// // And the uri we use to load this script in the webview
const scriptUri = webview.asWebviewUri( const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'out/compiled', 'problemPanel.js') vscode.Uri.joinPath(this.extensionUri, 'out/compiled', 'problemPanel.js')
); );
// Uri to load styles into webview
const stylesResetUri = webview.asWebviewUri( const stylesResetUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'media', 'reset.css') vscode.Uri.joinPath(this.extensionUri, 'media', 'reset.css')
); );
const stylesMainUri = webview.asWebviewUri( const stylesMainUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'media', 'vscode.css') vscode.Uri.joinPath(this.extensionUri, 'media', 'vscode.css')
); );
const cssUri = webview.asWebviewUri( const cssUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'out/compiled', 'problemPanel.css') vscode.Uri.joinPath(this.extensionUri, 'out/compiled', 'problemPanel.css')
); );
// // Use a nonce to only allow specific scripts to be run
const nonce = getNonce(); const nonce = getNonce();
return `<!DOCTYPE html> return `<!DOCTYPE html>

View File

@ -1,120 +1,89 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import urlJoin from 'url-join'; import type { WebviewMessageType, MessageType, ProblemData } from '../../src/problemPanel';
function postMessage(message: any) { function postMessage(message: MessageType) {
vscode.postMessage(message); vscode.postMessage(message);
} }
type ProblemData = { // let savedInputs: Map<number, { input: string; output: string }> = new Map();
id: number;
name: string;
pascalName: string;
sampleInput: string;
sampleOutput: string;
}[];
let savedInputs: Map<number, { input: string; output: string }> = new Map(); let activeProblemIndex = 0;
let activeProblem: ProblemData[0];
let sessionToken: string | undefined;
let problemData: ProblemData | undefined; let problemData: ProblemData | undefined;
let sampleInputText: HTMLTextAreaElement; let sampleInputValue: string;
let outputText: HTMLTextAreaElement; let outputValue: string;
let running = false; let running = false;
let webUrl: string | undefined;
$: if (problemData && problemData.length !== 0) { $: if (problemData && problemData.length !== 0) {
let first = problemData.at(0); activeProblemIndex = 0;
if (first) {
activeProblem = first;
}
} }
function resetInput() { function resetInput() {
sampleInputText.value = activeProblem.sampleInput; if (problemData) {
sampleInputValue = problemData[activeProblemIndex].sampleInput;
} else {
sampleInputValue = '';
}
} }
let contestId: number | undefined;
let teamId: number | undefined;
function onRun() { function onRun() {
if (!running && contestId && teamId) { // if (problemData !== undefined && running === false) {
postMessage({ // postMessage({
type: 'onRun', // type: 'requestRun',
value: { // value: {
problemPascalName: activeProblem.pascalName, // problemId: problemData[activeProblemIndex].id,
contestId: contestId, // input: sampleInputValue
teamId: teamId, // }
input: sampleInputText.value // });
} // }
});
running = true;
}
} }
function updateTextBoxes() { function updateTextBoxes() {
if (savedInputs.has(activeProblem.id)) { // if (savedInputs.has(activeProblem.id)) {
sampleInputText.value = savedInputs.get(activeProblem.id)!.input; // sampleInputText.value = savedInputs.get(activeProblem.id)!.input;
outputText.value = savedInputs.get(activeProblem.id)!.output; // outputText.value = savedInputs.get(activeProblem.id)!.output;
} else { // } else {
sampleInputText.value = activeProblem.sampleInput; if (problemData !== undefined) {
outputText.value = '[Run to get output]'; sampleInputValue = problemData[activeProblemIndex].sampleInput;
} }
outputValue = '[Run to get output]';
// }
} }
function onSubmit() { function onSubmit() {
if (teamId && contestId && sessionToken) { // if (teamId && contestId && sessionToken) {
postMessage({ // postMessage({
type: 'onSubmit', // type: 'onSubmit',
value: { // value: {
sessionToken: sessionToken, // sessionToken: sessionToken,
contestId: contestId, // contestId: contestId,
teamId: teamId, // teamId: teamId,
problemId: activeProblem.id, // problemId: activeProblem.id,
problemName: activeProblem.pascalName // problemName: activeProblem.pascalName
} // }
}); // });
} // }
} }
function onKill() { function onKill() {
postMessage({ type: 'onKill' }); // postMessage({ type: 'onKill' });
} }
async function fetchProblemData() {
if (sessionToken && webUrl) {
const res = await fetch(urlJoin(webUrl, `/api/contest/${sessionToken}`));
const data = await res.json();
if (data.success === true) {
problemData = data.problems as ProblemData;
contestId = data.contestId;
teamId = data.teamId;
}
}
}
window.addEventListener('message', async (event) => {
const message = (event as MessageEvent).data;
if (message.type === 'onSession') {
if (message.value !== '') {
sessionToken = message.value;
await fetchProblemData();
updateTextBoxes();
}
} else if (message.type === 'onOutput') {
outputText.value = message.value;
running = false;
} else if (message.type === 'onWebUrl') {
webUrl = message.value;
}
});
onMount(() => { onMount(() => {
postMessage({ type: 'onStartup' }); postMessage({ msg: 'onRequestProblemData' });
});
window.addEventListener('message', async (event) => {
const m = (event as MessageEvent).data as WebviewMessageType;
// if (message.msg === 'onOutput') {
// outputValue = message.value;
// running = false;
if (m.msg === 'onProblemData') {
problemData = m.data;
updateTextBoxes();
}
}); });
</script> </script>
@ -122,32 +91,32 @@
{#if problemData} {#if problemData}
<div class="tab-container"> <div class="tab-container">
{#each problemData as problem} {#each problemData as problem, i}
<button <button
on:click={() => { on:click={() => {
if (!running) { if (!running) {
savedInputs.set(activeProblem.id, { // savedInputs.set(activeProblem.id, {
input: sampleInputText.value, // input: sampleInputText.value,
output: outputText.value // output: outputText.value
}); // });
activeProblem = problem; activeProblemIndex = i;
updateTextBoxes(); updateTextBoxes();
} }
}} }}
id={`problem_${problem.id}`} id={`problem_${problem.id}`}
type="button" type="button"
class={'tab ' + (activeProblem.id == problem.id ? 'active' : '')}>{problem.name}</button class={'tab ' + (activeProblemIndex === i ? 'active' : '')}>{problem.name}</button
> >
{/each} {/each}
</div> </div>
{/if} {/if}
{#if activeProblem} {#if problemData !== undefined}
<h2>{activeProblem.name}</h2> <h2>{problemData[activeProblemIndex].name}</h2>
<div style="display:flex"> <div style="display:flex">
<div style="flex:1; margin-right:20px"> <div style="flex:1; margin-right:20px">
<h3>Sample Input (You can edit this!)</h3> <h3>Sample Input (You can edit this!)</h3>
<textarea bind:this={sampleInputText} /> <textarea bind:value={sampleInputValue} />
<button style="margin-top:5px" on:click={resetInput} type="button">Reset Input</button> <button style="margin-top:5px" on:click={resetInput} type="button">Reset Input</button>
</div> </div>
<div style="flex:1"> <div style="flex:1">
@ -157,7 +126,7 @@
<span class="loader"></span> <span class="loader"></span>
{/if} {/if}
</div> </div>
<textarea bind:this={outputText} disabled /> <textarea bind:value={outputValue} readonly />
{#if !running} {#if !running}
<button style="margin-top:5px" on:click={onRun} type="button">Run</button> <button style="margin-top:5px" on:click={onRun} type="button">Run</button>
{:else} {:else}

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { WebviewMessageType, MessageType, TeamData } from '../../src/SidebarProvider';
function postMessage(message: any) { function postMessage(message: MessageType) {
vscode.postMessage(message); vscode.postMessage(message);
} }
@ -10,49 +11,44 @@
let loggedIn = false; let loggedIn = false;
interface TeamData {
teamId: number;
contestId: number;
}
let teamData: TeamData | undefined; let teamData: TeamData | undefined;
function onClone() { function onClone() {
if (teamData) { if (teamData) {
postMessage({ postMessage({
type: 'onClone', msg: 'onClone',
value: { contestId: teamData.contestId, teamId: teamData.teamId } data: { contestId: teamData.contestId, teamId: teamData.teamId }
}); });
} }
} }
function onLogin() { function onLogin() {
postMessage({ postMessage({
type: 'requestLogin', msg: 'onLogin',
value: { teamname: teamname, password: password } data: { teamName: teamname, password: password }
}); });
} }
function onLogout() { function onLogout() {
postMessage({ postMessage({
type: 'requestLogout' msg: 'onLogout'
}); });
} }
function onTestAndSubmit() { function onTestAndSubmit() {
postMessage({ type: 'onTestAndSubmit' }); postMessage({ msg: 'onTestAndSubmit' });
} }
onMount(() => { onMount(() => {
postMessage({ type: 'onStartup' }); postMessage({ msg: 'onStartup' });
}); });
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
const message = (event as MessageEvent).data; const m = (event as MessageEvent).data as WebviewMessageType;
if (message.type === 'onLogin') { if (m.msg === 'onLogin') {
loggedIn = true; loggedIn = true;
teamData = message.value; teamData = m.data;
} else if (message.type === 'onLogout') { } else if (m.msg === 'onLogout') {
loggedIn = false; loggedIn = false;
teamData = undefined; teamData = undefined;
} }