Compare commits
10
Commits
02a7eb8090
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca32117a8a | ||
|
|
ecfa56bf5a | ||
|
|
77909c5684 | ||
|
|
663c085f34 | ||
|
|
272812a815 | ||
|
|
fd01ccf8af | ||
|
|
7877651ab0 | ||
|
|
7f0aa63843 | ||
|
|
9d72732cfd | ||
|
|
2ef3cae6eb |
Vendored
+2
-1
@@ -7,7 +7,8 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"]
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||
"preLaunchTask": "npm: compile"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "npm: compile",
|
||||
"type": "shell",
|
||||
"command": "npm run compile",
|
||||
"group": "build",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
# extension
|
||||
|
||||
The VSCode extension for SpeedCode
|
||||
|
||||
All the problems and their test cases [here](https://github.com/SpeedCodeHQ/problems)
|
||||
|
||||
Generated
+18
@@ -8,6 +8,7 @@
|
||||
"name": "speed-code",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/vscode": "^1.70.0",
|
||||
"tslint": "^6.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
@@ -41,6 +42,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
|
||||
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/vscode": {
|
||||
"version": "1.102.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.102.0.tgz",
|
||||
@@ -497,6 +508,13 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
+38
-5
@@ -6,15 +6,47 @@
|
||||
"engines": {
|
||||
"vscode": "^1.70.0"
|
||||
},
|
||||
"activationEvents": [],
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "speed-code.startSpeedRun",
|
||||
"title": "Hello World"
|
||||
"title": "Start Speed Run"
|
||||
},
|
||||
{
|
||||
"command": "speed-code.cancelSpeedRun",
|
||||
"title": "Cancel Speed Run"
|
||||
}
|
||||
]
|
||||
],
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "speed-code.submitSpeedRun",
|
||||
"key": "shift+enter",
|
||||
"when": "editorTextFocus"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"panel": [
|
||||
{
|
||||
"id": "speedCodeResultPanel",
|
||||
"title": "Speed Code",
|
||||
"icon": "./timer.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"speedCodeResultPanel": [
|
||||
{
|
||||
"type": "tree",
|
||||
"id": "speedCodeResultTab",
|
||||
"name": "Speed Code",
|
||||
"icon": "./timer.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "tsc -p ./",
|
||||
@@ -22,8 +54,9 @@
|
||||
"watch": "tsc -watch -p ./"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/vscode": "^1.70.0",
|
||||
"typescript": "^5.0.0",
|
||||
"tslint": "^6.0.0"
|
||||
"tslint": "^6.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+178
-7
@@ -1,11 +1,182 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as vscode from "vscode";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import testCode, { TestCase } from "./tester";
|
||||
import * as utils from "./utils";
|
||||
import { MyTreeDataProvider } from "./resultViewer";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const disposable = vscode.commands.registerCommand('speed-code.startSpeedRun', () => {
|
||||
vscode.window.showInformationMessage('Hello World from My Extension!');
|
||||
});
|
||||
let resetTimer = () => {};
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
const speedCodePath = path.join(os.homedir(), ".speed-code/");
|
||||
|
||||
if (!fs.existsSync(speedCodePath)) {
|
||||
fs.mkdirSync(speedCodePath);
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
export async function openAndFocusFile(filePath: string) {
|
||||
const document = await vscode.workspace.openTextDocument(filePath);
|
||||
await vscode.window.showTextDocument(document, {
|
||||
preview: false,
|
||||
preserveFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
const userCode = `
|
||||
function isPalindrome(s) {
|
||||
return s === s.split('').reverse().join('');
|
||||
}
|
||||
`;
|
||||
|
||||
const treeDataProvider = new MyTreeDataProvider();
|
||||
|
||||
const treeView = vscode.window.createTreeView("speedCodeResultTab", {
|
||||
treeDataProvider,
|
||||
});
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const statusBarItem = vscode.window.createStatusBarItem(
|
||||
vscode.StatusBarAlignment.Left,
|
||||
100
|
||||
);
|
||||
statusBarItem.tooltip = "Speedrun timer";
|
||||
statusBarItem.text = "No active Speedrun";
|
||||
statusBarItem.show();
|
||||
|
||||
let run:
|
||||
| undefined
|
||||
| {
|
||||
path: string;
|
||||
name: string;
|
||||
testCases: TestCase[];
|
||||
elapsed: number;
|
||||
timer: NodeJS.Timeout;
|
||||
};
|
||||
|
||||
resetTimer = () => {
|
||||
if (run) {
|
||||
clearInterval(run.timer);
|
||||
run = undefined;
|
||||
}
|
||||
statusBarItem.text = "No active Speedrun";
|
||||
};
|
||||
|
||||
resetTimer();
|
||||
updateTreeMessage();
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("speed-code.submitSpeedRun", () => {
|
||||
if (run) {
|
||||
try {
|
||||
const [isValid, res] = testCode(
|
||||
fs.readFileSync(run.path).toString(),
|
||||
run.testCases
|
||||
);
|
||||
|
||||
treeDataProvider.testResults = res;
|
||||
treeDataProvider.testSummary = {
|
||||
elapsed: run.elapsed,
|
||||
valid: isValid,
|
||||
};
|
||||
|
||||
if (isValid) {
|
||||
resetTimer();
|
||||
} else {
|
||||
treeDataProvider.blunders.push([run.elapsed, res]);
|
||||
}
|
||||
|
||||
treeDataProvider.refresh();
|
||||
updateTreeMessage();
|
||||
} catch (e) {
|
||||
treeDataProvider.blunders.push([run.elapsed, String(e)]);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("speed-code.startSpeedRun", async () => {
|
||||
const quickPick = vscode.window.createQuickPick();
|
||||
quickPick.placeholder = "Type to search";
|
||||
quickPick.matchOnDescription = true;
|
||||
quickPick.items = utils.problems.map((label) => ({ label }));
|
||||
quickPick.onDidChangeValue((value) => {
|
||||
quickPick.items = utils.problems
|
||||
.filter((prob) => prob.toLowerCase().includes(value.toLowerCase()))
|
||||
.map((label) => ({ label }));
|
||||
});
|
||||
quickPick.onDidAccept(async () => {
|
||||
const selection = quickPick.selectedItems[0];
|
||||
if (selection) {
|
||||
const name = selection.label.toLowerCase().replace(' ', '-');
|
||||
treeDataProvider.testSummary = undefined;
|
||||
treeDataProvider.testResults = [];
|
||||
treeDataProvider.blunders = [];
|
||||
treeDataProvider.refresh();
|
||||
const runFilePath = path.join(speedCodePath, name.replace('-', '_') + ".js");
|
||||
fs.writeFileSync(runFilePath, "");
|
||||
openAndFocusFile(runFilePath);
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/SpeedCodeHQ/problems/refs/heads/main/problems/${name}.json`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to fetch test cases for ${name}: ${response.statusText}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
run = {
|
||||
path: runFilePath,
|
||||
elapsed: 0,
|
||||
name,
|
||||
testCases: (await response.json()) as TestCase[],
|
||||
timer: setInterval(() => {
|
||||
if (!run) return;
|
||||
const currentTime = Date.now();
|
||||
run.elapsed = (currentTime - startTime) / 1000;
|
||||
statusBarItem.text = utils.format(run.elapsed);
|
||||
statusBarItem.color = "#fff";
|
||||
}, 10),
|
||||
};
|
||||
|
||||
updateTreeMessage();
|
||||
vscode.window.showInformationMessage("Speedrun Started!");
|
||||
}
|
||||
quickPick.hide();
|
||||
});
|
||||
quickPick.show();
|
||||
}),
|
||||
statusBarItem
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("speed-code.cancelSpeedRun", () => {
|
||||
resetTimer();
|
||||
treeDataProvider.testSummary = undefined;
|
||||
treeDataProvider.testResults = [];
|
||||
treeDataProvider.blunders = [];
|
||||
treeDataProvider.refresh();
|
||||
}),
|
||||
statusBarItem
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
resetTimer();
|
||||
}
|
||||
|
||||
function updateTreeMessage() {
|
||||
if (
|
||||
!treeDataProvider.testResults.length &&
|
||||
!treeDataProvider.blunders.length &&
|
||||
!treeDataProvider.testSummary
|
||||
) {
|
||||
treeView.message = "No results yet. Start a speedrun!";
|
||||
} else {
|
||||
treeView.message = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import * as vscode from "vscode";
|
||||
import { TestResult } from "./tester";
|
||||
import { format } from "./utils";
|
||||
|
||||
class RunTreeItem extends vscode.TreeItem {
|
||||
children?: RunTreeItem[];
|
||||
|
||||
constructor(
|
||||
label: string | vscode.TreeItemLabel,
|
||||
description?: string,
|
||||
children?: RunTreeItem[],
|
||||
expanded: boolean = false
|
||||
) {
|
||||
super(
|
||||
label,
|
||||
children
|
||||
? expanded
|
||||
? vscode.TreeItemCollapsibleState.Expanded
|
||||
: vscode.TreeItemCollapsibleState.Collapsed
|
||||
: vscode.TreeItemCollapsibleState.None
|
||||
);
|
||||
this.children = children;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
export class MyTreeDataProvider
|
||||
implements vscode.TreeDataProvider<RunTreeItem>
|
||||
{
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<
|
||||
RunTreeItem | undefined | void
|
||||
> = new vscode.EventEmitter();
|
||||
readonly onDidChangeTreeData: vscode.Event<RunTreeItem | undefined | void> =
|
||||
this._onDidChangeTreeData.event;
|
||||
|
||||
testResults: TestResult[] = [];
|
||||
testSummary: undefined | { elapsed: number; valid: boolean };
|
||||
blunders: [number, string | TestResult[]][] = [];
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
getTreeItem(element: RunTreeItem): RunTreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(element?: RunTreeItem): Thenable<RunTreeItem[]> {
|
||||
if (!element) {
|
||||
const items: RunTreeItem[] = [];
|
||||
|
||||
if (this.testSummary) {
|
||||
items.push(
|
||||
new RunTreeItem(
|
||||
`Speedrun Summary`,
|
||||
undefined,
|
||||
[
|
||||
new RunTreeItem("Time Elapsed", format(this.testSummary.elapsed)),
|
||||
new RunTreeItem(
|
||||
"Test Passed",
|
||||
this.testSummary.valid ? "✅ Passed" : "❌ Failed"
|
||||
),
|
||||
new RunTreeItem("Blunders", `${this.blunders.length}`),
|
||||
],
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.blunders.length > 0) {
|
||||
items.push(
|
||||
new RunTreeItem(
|
||||
`Blunders`,
|
||||
undefined,
|
||||
this.blunders.map((blunder, i) =>
|
||||
typeof blunder[1] === "string"
|
||||
? new RunTreeItem(blunder[1], format(blunder[0]))
|
||||
: new RunTreeItem(
|
||||
`Output Blunder ${i + 1}`,
|
||||
format(blunder[0]),
|
||||
blunder[1].map(
|
||||
(t) =>
|
||||
new RunTreeItem(
|
||||
`(${JSON.stringify(t.input)})`,
|
||||
undefined,
|
||||
[
|
||||
new RunTreeItem("Output", `${t.result}`),
|
||||
new RunTreeItem("Expected Output", `${t.expected}`),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.testResults.forEach((t) => {
|
||||
const item = new RunTreeItem(
|
||||
`(${JSON.stringify(t.input)})`,
|
||||
undefined,
|
||||
[
|
||||
new RunTreeItem("Output", `${t.result}`),
|
||||
new RunTreeItem("Expected Output", `${t.expected}`),
|
||||
]
|
||||
);
|
||||
item.description = t.valid ? "✅ Passed" : "❌ Failed";
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
return Promise.resolve(items);
|
||||
} else {
|
||||
return Promise.resolve(element.children || []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import vm from "vm";
|
||||
import assert from "assert";
|
||||
|
||||
export interface TestCase {
|
||||
input: any[];
|
||||
expected: any;
|
||||
}
|
||||
export interface TestResult extends TestCase {
|
||||
result: any;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export default function testCode(
|
||||
code: string,
|
||||
testCases: TestCase[]
|
||||
): [boolean, TestResult[]] {
|
||||
const context: Record<string, any> = {};
|
||||
vm.createContext(context);
|
||||
|
||||
const beforeKeys = new Set(Object.keys(context));
|
||||
|
||||
const script = new vm.Script(code);
|
||||
script.runInContext(context);
|
||||
|
||||
const afterKeys = Object.keys(context);
|
||||
const newKeys = afterKeys.filter((k) => !beforeKeys.has(k));
|
||||
|
||||
// Find the last function defined
|
||||
const funcs = newKeys
|
||||
.map((k) => context[k])
|
||||
.filter((v) => typeof v === "function");
|
||||
|
||||
if (funcs.length === 0) {
|
||||
throw new Error("No function was defined in the code.");
|
||||
}
|
||||
|
||||
const targetFn = funcs[funcs.length - 1];
|
||||
|
||||
const tests = testCases.map((t) => {
|
||||
const result = targetFn(...t.input);
|
||||
|
||||
try {
|
||||
assert.strictEqual(result, t.expected);
|
||||
return { ...t, valid: true, result };
|
||||
} catch {
|
||||
return { ...t, valid: false, result };
|
||||
}
|
||||
});
|
||||
|
||||
return [tests.every((t) => t.valid), tests];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const problems = ["Two Sum", "Palindrome", "Fibonacci", "Factorial", "Find Missing Number", "Anagram", "Reverse Words In a Sentence"];
|
||||
|
||||
export function format(timeSeconds: number): string {
|
||||
if (timeSeconds > 86400) {
|
||||
const days = Math.floor(timeSeconds / 86400);
|
||||
const hours = Math.floor(timeSeconds / 3600);
|
||||
const minutes = Math.floor(timeSeconds / 60);
|
||||
return `${days}:${hours - days * 24}:${minutes - hours * 60}:${(
|
||||
timeSeconds -
|
||||
minutes * 60
|
||||
).toFixed(3)}`;
|
||||
} else if (timeSeconds >= 3600) {
|
||||
const hours = Math.floor(timeSeconds / 3600);
|
||||
const minutes = Math.floor(timeSeconds / 60);
|
||||
return `${hours}:${minutes - hours * 60}:${(
|
||||
timeSeconds -
|
||||
minutes * 60
|
||||
).toFixed(3)}`;
|
||||
} else if (timeSeconds >= 60) {
|
||||
const minutes = Math.floor(timeSeconds / 60);
|
||||
return `${minutes}:${(timeSeconds - minutes * 60).toFixed(3)}`;
|
||||
} else {
|
||||
return `${timeSeconds}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-timer-icon lucide-timer"><line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
+2
-1
@@ -7,7 +7,8 @@
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vscode"]
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user