Compare commits

..
10 Commits
Author SHA1 Message Date
selimaj-dev ca32117a8a Added more problems 2025-08-07 07:12:17 -05:00
selimaj-dev ecfa56bf5a Live summary on submit 2025-08-07 06:53:22 -05:00
selimaj-dev 77909c5684 Online repo for problems 2025-08-06 23:45:41 +02:00
selimaj-dev 663c085f34 Visual modifications and fixes 2025-08-06 23:15:38 +02:00
selimaj-dev 272812a815 blunders 2025-08-06 22:52:32 +02:00
selimaj-dev fd01ccf8af Working analytics 2025-08-06 15:08:57 -05:00
selimaj-dev 7877651ab0 Slight improvements 2025-08-06 14:01:45 -05:00
selimaj-dev 7f0aa63843 Working example 2025-08-06 13:35:14 -05:00
selimaj-dev 9d72732cfd Javascript tester 2025-08-06 13:21:37 -05:00
selimaj-dev 2ef3cae6eb Working example with start cancel 2025-08-06 09:46:10 -05:00
11 changed files with 446 additions and 14 deletions
+2 -1
View File
@@ -7,7 +7,8 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"]
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
}
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm: compile",
"type": "shell",
"command": "npm run compile",
"group": "build",
"problemMatcher": []
}
]
}
+3
View File
@@ -1,2 +1,5 @@
# extension
The VSCode extension for SpeedCode
All the problems and their test cases [here](https://github.com/SpeedCodeHQ/problems)
+18
View File
@@ -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",
+37 -4
View File
@@ -6,24 +6,57 @@
"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 ./",
"compile": "tsc -p ./",
"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
View File
@@ -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 = "";
}
}
+116
View File
@@ -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 || []);
}
}
}
+51
View File
@@ -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];
}
+25
View File
@@ -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}`;
}
}
+1
View File
@@ -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
View File
@@ -7,7 +7,8 @@
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
"skipLibCheck": true,
"types": ["node", "vscode"]
},
"exclude": ["node_modules", ".vscode-test"]
}