From acbcc57402b3a616aa47a1ff64e8ae72f7aaa2cd Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 12:14:00 +0100 Subject: [PATCH 01/10] Simple auth --- src/client.ts | 15 +++++++-------- src/server.ts | 16 +++++++++++++--- src/types.ts | 9 +++++++++ 3 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 src/types.ts diff --git a/src/client.ts b/src/client.ts index bfe0b32..95c0f0a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,11 +1,10 @@ -export interface LiveRunMove { - latency: number; - cursor: number; - changes?: { - from: number; - to: number; - insert: string; - }; +var cookies: string | undefined; + +export function getCookies(): string | undefined { + return cookies; +} +export function setCookies(c: string) { + cookies = c; } export async function submitRun() {} diff --git a/src/server.ts b/src/server.ts index ce3f2b2..2e3a329 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,15 +2,16 @@ import * as vscode from "vscode"; import express from "express"; import * as http from "http"; import cors from "cors"; +import { getCookies, setCookies } from "./client"; -let server: http.Server | undefined; +var server: http.Server | undefined; const app = express(); +app.use(express.json()); app.use( cors({ origin: (origin, callback) => { - // allow requests with no origin (e.g., direct curl) if ( !origin || origin.includes("localhost") || @@ -25,10 +26,19 @@ app.use( }), ); +app.post("/auth", (req, res) => { + const { saveCookies } = req.body; + + setCookies(saveCookies); + + res.json({ ok: true }); +}); + app.get("/start-run/:problemId", (req, res) => { const { problemId } = req.params; - // trigger VS Code command + if (!getCookies()) return res.json({ ok: false, code: "auth" }); + vscode.commands.executeCommand("devrun-vscode.startRun", problemId); res.json({ ok: true, problemId }); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3ba77b9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export interface LiveRunMove { + latency: number; + cursor: number; + changes?: { + from: number; + to: number; + insert: string; + }; +} From a4b07f4fd2ffd0f864851224b7a34d761ed34c12 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 12:21:15 +0100 Subject: [PATCH 02/10] command hygiene --- package.json | 22 +++++++++++++++++++++- src/run.ts | 4 ++++ src/server.ts | 12 ++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d5e8bf..fe4bea0 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,27 @@ "command": "devrun-vscode.stopServer", "title": "DevRun: Stop the run server (for quick run starts)" } - ] + ], + "menus": { + "commandPalette": [ + { + "command": "devrun-vscode.startRun", + "when": "!devrun.runActive" + }, + { + "command": "devrun-vscode.stopRun", + "when": "devrun.runActive" + }, + { + "command": "devrun-vscode.startServer", + "when": "!devrun.serverActive" + }, + { + "command": "devrun-vscode.stopServer", + "when": "devrun.serverActive" + } + ] + } }, "scripts": { "vscode:prepublish": "npm run compile", diff --git a/src/run.ts b/src/run.ts index fa245f9..0fc57ae 100644 --- a/src/run.ts +++ b/src/run.ts @@ -53,6 +53,8 @@ export async function startRun( .padStart(2, "0")}`; }, 50), }; + + await vscode.commands.executeCommand("setContext", "devrun.runActive", true); } export async function stopRun() { @@ -60,4 +62,6 @@ export async function stopRun() { clearTimeout(currentRun.interval); currentRun = null; } + + await vscode.commands.executeCommand("setContext", "devrun.runActive", false); } diff --git a/src/server.ts b/src/server.ts index 2e3a329..317875e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -52,6 +52,12 @@ export async function startServer(statusBarItem: vscode.StatusBarItem) { server = app.listen(63780, "127.0.0.1", () => { setTimeout(() => (statusBarItem.text = "$(clock) No active run"), 500); }); + + await vscode.commands.executeCommand( + "setContext", + "devrun.serverActive", + true, + ); } export async function stopServer(statusBarItem: vscode.StatusBarItem) { @@ -67,4 +73,10 @@ export async function stopServer(statusBarItem: vscode.StatusBarItem) { server = undefined; setTimeout(() => (statusBarItem.text = "$(clock) No active run"), 500); + + await vscode.commands.executeCommand( + "setContext", + "devrun.serverActive", + false, + ); } From a79b653b05f1a6a459332837ac1a25f6a417dd73 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 12:26:37 +0100 Subject: [PATCH 03/10] Auto start --- package.json | 10 ++++++++++ src/extension.ts | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fe4bea0..174598d 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,16 @@ "when": "devrun.serverActive" } ] + }, + "configuration": { + "title": "DevRun", + "properties": { + "devrun.autoStartServer": { + "type": "boolean", + "default": false, + "description": "Automatically start the server when vscode starts" + } + } } }, "scripts": { diff --git a/src/extension.ts b/src/extension.ts index 66f7d2b..173d0b3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,8 +3,7 @@ import { currentRun, startRun, stopRun } from "./run"; import { startServer, stopServer } from "./server"; export function activate(context: vscode.ExtensionContext) { - console.log("dev-run Activated"); - + const config = vscode.workspace.getConfiguration("devrun"); const statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, 10, @@ -49,6 +48,14 @@ export function activate(context: vscode.ExtensionContext) { stopServer(statusBarItem), ), ); + console.log("dev-run Activated"); + + const autoStart = config.get("autoStartServer"); + + if (autoStart) { + startServer(statusBarItem); + console.log("dev-run server auto started!"); + } } export function deactivate() { From 6f418661c4d3dab47e74ba653d33c2ed808e1ab0 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 12:50:21 +0100 Subject: [PATCH 04/10] Security checks --- src/extension.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 173d0b3..f3c08b5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,9 @@ import * as vscode from "vscode"; import { currentRun, startRun, stopRun } from "./run"; import { startServer, stopServer } from "./server"; +import os from "os"; +import { exec } from "child_process"; +import net from "net"; export function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration("devrun"); @@ -50,14 +53,99 @@ export function activate(context: vscode.ExtensionContext) { ); console.log("dev-run Activated"); + // Security check - Make sure there aren't any malicious servers currently running const autoStart = config.get("autoStartServer"); - if (autoStart) { - startServer(statusBarItem); - console.log("dev-run server auto started!"); - } + canConnect(63780) + .then((canConnect) => { + if (canConnect) { + console.error( + "Unable to start due to another process listening on port 63780", + ); + + console.error("Possible malicious dev-run instance running"); + + vscode.window + .showErrorMessage( + "Possible malicious dev-run instance running", + "Kill the process listening on port 63780", + ) + .then(async (response) => { + if (!response) return; + + if (response === "Kill the process listening on port 63780") { + await killProcessOnPort(63780); + + if (autoStart) { + startServer(statusBarItem); + console.log("dev-run server auto started!"); + } + } + }); + + return; + } + + if (autoStart) { + startServer(statusBarItem); + console.log("dev-run server auto started!"); + } + }) + .catch(() => {}); } export function deactivate() { stopRun(); } + +export function canConnect( + port: number, + host = "127.0.0.1", + timeout = 1000, +): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + + socket.setTimeout(timeout); + + socket.once("connect", () => { + socket.destroy(); + resolve(true); + }); + + socket.once("timeout", () => { + socket.destroy(); + resolve(false); + }); + + socket.once("error", () => { + resolve(false); + }); + + socket.connect(port, host); + }); +} + +export function killProcessOnPort(port: number): Promise { + return new Promise((resolve, reject) => { + const platform = os.platform(); + + let cmd: string; + + if (platform === "win32") { + // Windows + cmd = `for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port}') do taskkill /PID %a /F`; + } else { + // macOS / Linux + cmd = `lsof -ti tcp:${port} | xargs kill -9`; + } + + exec(cmd, (error) => { + if (error) { + resolve(); + } else { + resolve(); + } + }); + }); +} From f944de31e2b9a3bde337c2046e3cf9222621a2e3 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 14:15:16 +0100 Subject: [PATCH 05/10] working run api calls --- package-lock.json | 132 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/client.ts | 33 +++++++++++- src/run.ts | 6 +++ 4 files changed, 171 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index be2cbbb..6df8e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "axios": "^1.13.5", "cors": "^2.8.6", "express": "^5.2.1" }, @@ -769,6 +770,23 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1063,6 +1081,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1186,6 +1216,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1304,6 +1343,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1717,6 +1771,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1734,6 +1808,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1972,6 +2083,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3090,6 +3216,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 174598d..2ea622a 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "dependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "axios": "^1.13.5", "cors": "^2.8.6", "express": "^5.2.1" } diff --git a/src/client.ts b/src/client.ts index 95c0f0a..3f9b1b5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,3 +1,5 @@ +import axios from "axios"; + var cookies: string | undefined; export function getCookies(): string | undefined { @@ -7,6 +9,35 @@ export function setCookies(c: string) { cookies = c; } -export async function submitRun() {} +export async function addRun(problem: string, mode: string): Promise { + return ( + await axios.put( + "http://localhost:3000/api/run", + { + problem, + category: mode, + }, + { + headers: { + Cookie: cookies, + }, + }, + ) + ).data.runId; +} + +export async function submitRun(runId: string) { + await axios.post( + "http://localhost:3000/api/run", + { + runId, + }, + { + headers: { + Cookie: cookies, + }, + }, + ); +} export async function addRunMoves() {} diff --git a/src/run.ts b/src/run.ts index 0fc57ae..8a1c99a 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,9 +1,11 @@ import * as vscode from "vscode"; +import { addRun, submitRun } from "./client"; export var currentRun: { interval: NodeJS.Timeout; problemId: string; start: Date; + id: string; } | null = null; export async function startRun( @@ -38,7 +40,10 @@ export async function startRun( let start = Date.now(); + const runId = await addRun(problemId, mode); + currentRun = { + id: runId, problemId, start: new Date(), interval: setInterval(() => { @@ -60,6 +65,7 @@ export async function startRun( export async function stopRun() { if (currentRun) { clearTimeout(currentRun.interval); + submitRun(currentRun.id); currentRun = null; } From f57fb22afc7aa4cb7cc586d1ebf64f992e544a5d Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 14:16:35 +0100 Subject: [PATCH 06/10] more accurate timer between client and api --- src/run.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/run.ts b/src/run.ts index 8a1c99a..9946190 100644 --- a/src/run.ts +++ b/src/run.ts @@ -38,10 +38,10 @@ export async function startRun( if (!mode) return; - let start = Date.now(); - const runId = await addRun(problemId, mode); + let start = Date.now(); + currentRun = { id: runId, problemId, From d88dba23027521e466b6246f9065c3e4e4feaf05 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 16:55:32 +0100 Subject: [PATCH 07/10] Simple live moves --- src/client.ts | 16 ++++++++++++- src/move.ts | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/run.ts | 8 ++++++- src/types.ts | 1 + 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/move.ts diff --git a/src/client.ts b/src/client.ts index 3f9b1b5..219a67c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { LiveRunMove } from "./types"; var cookies: string | undefined; @@ -40,4 +41,17 @@ export async function submitRun(runId: string) { ); } -export async function addRunMoves() {} +export async function addRunMoves(runId: string, moves: LiveRunMove[]) { + await axios.post( + "http://localhost:3000/api/run/move", + { + runId, + moves, + }, + { + headers: { + Cookie: cookies, + }, + }, + ); +} diff --git a/src/move.ts b/src/move.ts new file mode 100644 index 0000000..2341b71 --- /dev/null +++ b/src/move.ts @@ -0,0 +1,65 @@ +import * as vscode from "vscode"; +import { LiveRunMove } from "./types"; +import { addRunMoves } from "./client"; +import { randomInt } from "crypto"; + +let lastText = ""; +let lastCursor = 0; +let moveId = 0; + +export function startMonitoring(runId: string) { + lastText = ""; + + return setInterval(() => { + const editor = vscode.window.activeTextEditor; + if (!editor) return; + + const currentText = editor.document.getText(); + const cursorOffset = editor.document.offsetAt(editor.selection.active); + + let change: { from: number; to: number; insert: string } | undefined; + + if (currentText !== lastText) { + let start = 0; + while ( + start < lastText.length && + start < currentText.length && + lastText[start] === currentText[start] + ) { + start++; + } + + let endOld = lastText.length - 1; + let endNew = currentText.length - 1; + while ( + endOld >= start && + endNew >= start && + lastText[endOld] === currentText[endNew] + ) { + endOld--; + endNew--; + } + + change = { + from: start, + to: endOld + 1, // ⚡ correct range in old text + insert: currentText.slice(start, endNew + 1), + }; + + lastText = currentText; + } + + const move: LiveRunMove = { + latency: 300, + cursor: cursorOffset, + changes: change, + moveId, + }; + + if (move.changes || move.cursor !== lastCursor) { + addRunMoves(runId, [move]); + } + + lastCursor = move.cursor; + }, 1000); +} diff --git a/src/run.ts b/src/run.ts index 9946190..67eae81 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,8 +1,10 @@ import * as vscode from "vscode"; import { addRun, submitRun } from "./client"; +import { startMonitoring } from "./move"; export var currentRun: { interval: NodeJS.Timeout; + monitorInterval: NodeJS.Timeout; problemId: string; start: Date; id: string; @@ -57,6 +59,7 @@ export async function startRun( .toString() .padStart(2, "0")}`; }, 50), + monitorInterval: startMonitoring(runId), }; await vscode.commands.executeCommand("setContext", "devrun.runActive", true); @@ -64,8 +67,11 @@ export async function startRun( export async function stopRun() { if (currentRun) { - clearTimeout(currentRun.interval); + clearInterval(currentRun.interval); + clearInterval(currentRun.monitorInterval); + submitRun(currentRun.id); + currentRun = null; } diff --git a/src/types.ts b/src/types.ts index 3ba77b9..d2f1bbc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,4 +6,5 @@ export interface LiveRunMove { to: number; insert: string; }; + moveId: number; } From 7983775492ba7df896339ab2783b149a1b9a5321 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 17:43:00 +0100 Subject: [PATCH 08/10] Fixed issues --- src/move.ts | 85 ++++++++++++++++++++++------------------------------- src/run.ts | 6 ++-- 2 files changed, 37 insertions(+), 54 deletions(-) diff --git a/src/move.ts b/src/move.ts index 2341b71..9689abc 100644 --- a/src/move.ts +++ b/src/move.ts @@ -1,65 +1,50 @@ import * as vscode from "vscode"; import { LiveRunMove } from "./types"; import { addRunMoves } from "./client"; -import { randomInt } from "crypto"; - -let lastText = ""; -let lastCursor = 0; -let moveId = 0; export function startMonitoring(runId: string) { - lastText = ""; + let moveId = 0; + let moves: LiveRunMove[] = []; + let lastLatency = Date.now(); - return setInterval(() => { + const docSub = vscode.workspace.onDidChangeTextDocument((e) => { const editor = vscode.window.activeTextEditor; - if (!editor) return; + if (!editor || e.document !== editor.document) return; + + const edit = e.contentChanges[0]; + if (!edit) return; + + const now = Date.now(); + const latency = capToZeroAbove(now - lastLatency, 350); + lastLatency = now; - const currentText = editor.document.getText(); const cursorOffset = editor.document.offsetAt(editor.selection.active); - let change: { from: number; to: number; insert: string } | undefined; - - if (currentText !== lastText) { - let start = 0; - while ( - start < lastText.length && - start < currentText.length && - lastText[start] === currentText[start] - ) { - start++; - } - - let endOld = lastText.length - 1; - let endNew = currentText.length - 1; - while ( - endOld >= start && - endNew >= start && - lastText[endOld] === currentText[endNew] - ) { - endOld--; - endNew--; - } - - change = { - from: start, - to: endOld + 1, // ⚡ correct range in old text - insert: currentText.slice(start, endNew + 1), - }; - - lastText = currentText; - } - - const move: LiveRunMove = { - latency: 300, + moves.push({ + latency, cursor: cursorOffset, - changes: change, - moveId, - }; + changes: { + from: edit.rangeOffset, + to: edit.rangeOffset + edit.rangeLength, + insert: edit.text, + }, + moveId: moveId++, + }); + }); - if (move.changes || move.cursor !== lastCursor) { - addRunMoves(runId, [move]); + const sendInterval = setInterval(() => { + if (moves.length) { + addRunMoves(runId, moves); + moves = []; } + }, 3000); - lastCursor = move.cursor; - }, 1000); + return () => { + docSub.dispose(); + clearInterval(sendInterval); + }; +} + +function capToZeroAbove(value: number, max: number) { + return value > max ? 0 : value; } diff --git a/src/run.ts b/src/run.ts index 67eae81..75a71a9 100644 --- a/src/run.ts +++ b/src/run.ts @@ -4,7 +4,7 @@ import { startMonitoring } from "./move"; export var currentRun: { interval: NodeJS.Timeout; - monitorInterval: NodeJS.Timeout; + stopMonitoring: () => void; problemId: string; start: Date; id: string; @@ -48,6 +48,7 @@ export async function startRun( id: runId, problemId, start: new Date(), + stopMonitoring: startMonitoring(runId), interval: setInterval(() => { const elapsed = Date.now() - start; @@ -59,7 +60,6 @@ export async function startRun( .toString() .padStart(2, "0")}`; }, 50), - monitorInterval: startMonitoring(runId), }; await vscode.commands.executeCommand("setContext", "devrun.runActive", true); @@ -68,8 +68,6 @@ export async function startRun( export async function stopRun() { if (currentRun) { clearInterval(currentRun.interval); - clearInterval(currentRun.monitorInterval); - submitRun(currentRun.id); currentRun = null; From 2622651dc32bba3fd82754b1cd45efd8b90e7db2 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 18:13:15 +0100 Subject: [PATCH 09/10] Fixed issues --- src/move.ts | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/move.ts b/src/move.ts index 9689abc..7e11248 100644 --- a/src/move.ts +++ b/src/move.ts @@ -6,6 +6,24 @@ export function startMonitoring(runId: string) { let moveId = 0; let moves: LiveRunMove[] = []; let lastLatency = Date.now(); + let idleTimeout: NodeJS.Timeout | undefined; + let isSending = false; + + const SEND_IDLE_MS = 350; + + const flushMoves = async () => { + if (!moves.length || isSending) return; + + isSending = true; + const toSend = moves; + moves = []; + + try { + await addRunMoves(runId, toSend); + } finally { + isSending = false; + } + }; const docSub = vscode.workspace.onDidChangeTextDocument((e) => { const editor = vscode.window.activeTextEditor; @@ -15,7 +33,7 @@ export function startMonitoring(runId: string) { if (!edit) return; const now = Date.now(); - const latency = capToZeroAbove(now - lastLatency, 350); + const latency = capToZeroAbove(now - lastLatency, SEND_IDLE_MS); lastLatency = now; const cursorOffset = editor.document.offsetAt(editor.selection.active); @@ -30,18 +48,20 @@ export function startMonitoring(runId: string) { }, moveId: moveId++, }); - }); - const sendInterval = setInterval(() => { - if (moves.length) { - addRunMoves(runId, moves); - moves = []; + if (idleTimeout) { + clearTimeout(idleTimeout); } - }, 3000); + + idleTimeout = setTimeout(() => { + flushMoves(); + }, SEND_IDLE_MS); + }); return () => { docSub.dispose(); - clearInterval(sendInterval); + if (idleTimeout) clearTimeout(idleTimeout); + flushMoves(); }; } From c9ad06a4417c09cd915a1b9807ead6cf58967eed Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 18:32:51 +0100 Subject: [PATCH 10/10] fixed issues --- package-lock.json | 14 ++++++ package.json | 2 + src/move.ts | 106 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 90 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6df8e4b..126fedc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.1", "dependencies": { "@types/cors": "^2.8.19", + "@types/diff-match-patch": "^1.0.36", "@types/express": "^5.0.6", "axios": "^1.13.5", "cors": "^2.8.6", + "diff-match-patch": "^1.0.5", "express": "^5.2.1" }, "devDependencies": { @@ -302,6 +304,12 @@ "@types/node": "*" } }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -1244,6 +1252,12 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", diff --git a/package.json b/package.json index 2ea622a..939169a 100644 --- a/package.json +++ b/package.json @@ -84,9 +84,11 @@ }, "dependencies": { "@types/cors": "^2.8.19", + "@types/diff-match-patch": "^1.0.36", "@types/express": "^5.0.6", "axios": "^1.13.5", "cors": "^2.8.6", + "diff-match-patch": "^1.0.5", "express": "^5.2.1" } } diff --git a/src/move.ts b/src/move.ts index 7e11248..cd0ca43 100644 --- a/src/move.ts +++ b/src/move.ts @@ -1,70 +1,112 @@ import * as vscode from "vscode"; +import { diff_match_patch, Diff } from "diff-match-patch"; import { LiveRunMove } from "./types"; import { addRunMoves } from "./client"; export function startMonitoring(runId: string) { + const dmp = new diff_match_patch(); + let moveId = 0; let moves: LiveRunMove[] = []; - let lastLatency = Date.now(); + + let lastText = ""; + let lastEventTime = Date.now(); let idleTimeout: NodeJS.Timeout | undefined; let isSending = false; - const SEND_IDLE_MS = 350; + const IDLE_MS = 350; - const flushMoves = async () => { + const editor = vscode.window.activeTextEditor; + if (editor) { + lastText = editor.document.getText(); + } + + async function flush() { if (!moves.length || isSending) return; isSending = true; - const toSend = moves; + const batch = moves; moves = []; try { - await addRunMoves(runId, toSend); + await addRunMoves(runId, batch); } finally { isSending = false; } - }; + } - const docSub = vscode.workspace.onDidChangeTextDocument((e) => { + const readInterval = setInterval(() => { const editor = vscode.window.activeTextEditor; - if (!editor || e.document !== editor.document) return; + if (!editor) return; - const edit = e.contentChanges[0]; - if (!edit) return; + const newText = editor.document.getText(); + if (newText === lastText) return; const now = Date.now(); - const latency = capToZeroAbove(now - lastLatency, SEND_IDLE_MS); - lastLatency = now; + const latency = capLatency(now - lastEventTime, IDLE_MS); + lastEventTime = now; - const cursorOffset = editor.document.offsetAt(editor.selection.active); + const diffs = dmp.diff_main(lastText, newText); + dmp.diff_cleanupEfficiency(diffs); - moves.push({ - latency, - cursor: cursorOffset, - changes: { - from: edit.rangeOffset, - to: edit.rangeOffset + edit.rangeLength, - insert: edit.text, - }, - moveId: moveId++, - }); + const changes = diffsToChanges(diffs); - if (idleTimeout) { - clearTimeout(idleTimeout); + lastText = newText; + + for (const change of changes) { + moves.push({ + moveId: moveId++, + latency, + cursor: editor.document.offsetAt(editor.selection.active), + changes: change, + }); } - idleTimeout = setTimeout(() => { - flushMoves(); - }, SEND_IDLE_MS); - }); + if (idleTimeout) clearTimeout(idleTimeout); + idleTimeout = setTimeout(flush, IDLE_MS); + }, 500); return () => { - docSub.dispose(); + clearInterval(readInterval); if (idleTimeout) clearTimeout(idleTimeout); - flushMoves(); + flush(); }; } -function capToZeroAbove(value: number, max: number) { +function diffsToChanges(diffs: Diff[]) { + const changes: { + from: number; + to: number; + insert: string; + }[] = []; + + let offset = 0; + + for (const [op, text] of diffs) { + if (op === 0) { + // equal + offset += text.length; + } else if (op === -1) { + // delete + changes.push({ + from: offset, + to: offset + text.length, + insert: "", + }); + } else if (op === 1) { + // insert + changes.push({ + from: offset, + to: offset, + insert: text, + }); + offset += text.length; + } + } + + return changes; +} + +function capLatency(value: number, max: number) { return value > max ? 0 : value; }