├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── agent ├── agent.rb └── example.rb ├── bin └── build └── extension ├── .prettierrc ├── package-lock.json ├── package.json ├── src └── extension.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Launch extension", 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "runtimeExecutable": "${execPath}", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceRoot}" 10 | ], 11 | "outFiles": [ 12 | "${workspaceRoot}/out/**/*.js" 13 | ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "type": "npm", 5 | "script": "watch", 6 | "problemMatcher": "$tsc-watch", 7 | "isBackground": true, 8 | "presentation": { 9 | "reveal": "never" 10 | }, 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Light up running code 2 | 3 | ## What is this? 4 | 5 | A quick and dirty proof-of-concept of an idea I had – what if when you ran code, the relevant lines lit up in your text editor? And maybe those lines which ran the most lit up the brightest? 6 | 7 | ## Demo 8 | 9 | ![Screen recording](https://user-images.githubusercontent.com/1694410/82281107-82e14b80-995e-11ea-9229-ed5b7252a0a7.gif) 10 | 11 | ## How this works 12 | 13 | The implementation here consists of a Ruby agent and a VS Code extension. The agent profiles running code and pings messages to a socket, which the extension listens for and surfaces in the editor. 14 | 15 | ## Run this yourself locally 16 | 17 | 1. Clone the repository 18 | 2. Run the build/watch script: `bin/build` 19 | 3. Open it in VS Code 20 | 4. Press F5 (or Run > Start Debugging) 21 | 5. Open the `agent` directory in this respository in the resulting VS Code window (labelled ‘Extension Development Host’) 22 | 6. Run the example script in the `agent` directory, `ruby example.rb` 23 | 7. Watch the lines of code light up as they run 24 | -------------------------------------------------------------------------------- /agent/agent.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Agent 4 | EVENTS = [:line, :call, :raise, :return, :end] 5 | RESET = "RESET" 6 | HOST = "localhost" 7 | PORT = 8000 8 | 9 | def self.run 10 | emit RESET 11 | trace = TracePoint.new(*EVENTS) do |tp| 12 | line_number = tp.lineno.to_i - 1 13 | path = tp.path.include?(Dir.pwd) ? tp.path : "#{Dir.pwd}/#{tp.path}" 14 | emit "#{path} #{line_number}" 15 | end 16 | trace.enable 17 | yield 18 | trace.disable 19 | end 20 | 21 | def self.emit(message) 22 | TCPSocket.open(HOST, PORT).tap do |client| 23 | client.puts message 24 | client.close 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /agent/example.rb: -------------------------------------------------------------------------------- 1 | require_relative "./agent" 2 | 3 | def main 4 | puts "0" 5 | sleep 1 6 | 5.times do 7 | subroutine 8 | sleep 0.5 9 | end 10 | puts "1" 11 | sleep 1 12 | end 13 | 14 | def subroutine 15 | puts "a" 16 | 3.times do 17 | sleep 0.1 18 | puts "b" 19 | end 20 | puts "c" 21 | end 22 | 23 | def dead_code 24 | # not called 25 | end 26 | 27 | Agent.run { main } 28 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | cd extension 2 | npm install 3 | npm run watch -------------------------------------------------------------------------------- /extension/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /extension/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "light-up-running-code", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "12.12.39", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.39.tgz", 10 | "integrity": "sha512-pADGfwnDkr6zagDwEiCVE4yQrv7XDkoeVa4OfA9Ju/zRTk6YNDLGtQbkdL4/56mCQQCs4AhNrBIag6jrp7ZuOg==", 11 | "dev": true 12 | }, 13 | "@types/vscode": { 14 | "version": "1.33.0", 15 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.33.0.tgz", 16 | "integrity": "sha512-JSmGiValbrcG5g20jjCfKakLiuWyrcjVezj+SEAEZ4klXQktE5EtowuGlkLVqbkiBK4iY5wy/4yW8OjecuHnjQ==", 17 | "dev": true 18 | }, 19 | "prettier": { 20 | "version": "2.0.5", 21 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", 22 | "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", 23 | "dev": true 24 | }, 25 | "typescript": { 26 | "version": "3.8.3", 27 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 28 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 29 | "dev": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "light-up-running-code", 3 | "description": "Proof of concept: light up the lines of code which run", 4 | "version": "0.0.1", 5 | "publisher": "vscode-samples", 6 | "license": "MIT", 7 | "engines": { 8 | "vscode": "^1.32.0" 9 | }, 10 | "activationEvents": [ 11 | "*" 12 | ], 13 | "main": "./out/extension.js", 14 | "scripts": { 15 | "watch": "tsc -watch -p ./" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^12.12.39", 19 | "@types/vscode": "^1.32.0", 20 | "typescript": "^3.8.3", 21 | "prettier": "^2.0.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import * as net from "net" 3 | 4 | type PathHits = { [line: string]: number } 5 | type Hits = { [path: string]: PathHits } 6 | type DecorationType = "heavy" | "medium" | "light" 7 | 8 | const PORT = 8000 9 | const HOST = "localhost" 10 | const counts = { light: 1, medium: 3, heavy: 10 } 11 | let timeout: NodeJS.Timer | undefined = undefined 12 | let hits: Hits = {} 13 | let activeEditor = vscode.window.activeTextEditor 14 | 15 | const resetHits = () => { 16 | hits = {} 17 | } 18 | 19 | const addHit = (string: string) => { 20 | const [path, line] = string.split(" ").map((s) => s.trim()) 21 | hits[path] = hits[path] || {} 22 | hits[path][line] = hits[path][line] || 0 23 | hits[path][line]++ 24 | } 25 | 26 | const createDecorationType = (backgroundColor: string) => 27 | vscode.window.createTextEditorDecorationType({ 28 | backgroundColor, 29 | isWholeLine: true, 30 | }) 31 | const heavy = createDecorationType("#FFFFCC30") 32 | const medium = createDecorationType("#FFFFCC20") 33 | const light = createDecorationType("#FFFFCC10") 34 | 35 | const getDecorations = (type: DecorationType, hits: PathHits) => { 36 | if (!activeEditor) return [] 37 | const { document } = activeEditor 38 | const minCount = counts[type] 39 | const maxCount = Object.values(counts).find((c) => c > minCount) || Infinity 40 | const decorations: vscode.DecorationOptions[] = [] 41 | Object.keys(hits).forEach((lineNumber) => { 42 | const count = hits[lineNumber] 43 | if (count >= minCount && count < maxCount) { 44 | const line = document.lineAt(parseInt(lineNumber)) 45 | decorations.push({ 46 | range: new vscode.Range(line.range.start, line.range.end), 47 | hoverMessage: `ran ${count} time${count === 1 ? "" : "s"}`, 48 | }) 49 | } 50 | }) 51 | return decorations 52 | } 53 | 54 | const updateDecorations = () => { 55 | if (!activeEditor) return 56 | const pathHits = hits[activeEditor.document.uri.path] 57 | activeEditor.setDecorations(heavy, getDecorations("heavy", pathHits)) 58 | activeEditor.setDecorations(medium, getDecorations("medium", pathHits)) 59 | activeEditor.setDecorations(light, getDecorations("light", pathHits)) 60 | } 61 | 62 | const triggerUpdateDecorations = () => { 63 | if (!activeEditor) return 64 | if (timeout) clearTimeout(timeout) 65 | timeout = setTimeout(updateDecorations, 500) 66 | } 67 | 68 | export const activate = (context: vscode.ExtensionContext) => { 69 | triggerUpdateDecorations() 70 | 71 | vscode.window.onDidChangeActiveTextEditor( 72 | (editor) => { 73 | activeEditor = editor 74 | triggerUpdateDecorations() 75 | }, 76 | null, 77 | context.subscriptions 78 | ) 79 | } 80 | 81 | net 82 | .createServer((socket) => { 83 | socket.on("data", (buffer) => { 84 | const data = buffer.toString() 85 | if (data.includes("RESET")) { 86 | resetHits() 87 | } else { 88 | addHit(data) 89 | updateDecorations() 90 | } 91 | }) 92 | socket.pipe(socket) 93 | }) 94 | .listen(PORT, HOST) 95 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "src" 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | --------------------------------------------------------------------------------