From 2eafb29abd79965314d19aeb1c715ef05b0f5c89 Mon Sep 17 00:00:00 2001 From: liviacutra Date: Thu, 19 Mar 2026 02:58:57 -0400 Subject: [PATCH 01/15] Fixed city rendering, multiple files, layout algorithm, improved sorting --- src/JavaFileWatcher.ts | 13 ++ src/extension.ts | 300 +++++++++++++++++++++++++++++------------ 2 files changed, 230 insertions(+), 83 deletions(-) diff --git a/src/JavaFileWatcher.ts b/src/JavaFileWatcher.ts index 9cfcd5c..444f773 100644 --- a/src/JavaFileWatcher.ts +++ b/src/JavaFileWatcher.ts @@ -83,6 +83,19 @@ export class JavaFileWatcher { }); } } + + broadcast(message: any) { + if (this._webviews.length === 0) { + console.log("[broadcast] no webviews yet"); + return; + } + + for (const v of this._webviews) { + v.postMessage(message).then(delivered => { + console.log("[broadcast] delivered:", delivered); + }); + } + } dispose(){ this._watcher.dispose(); diff --git a/src/extension.ts b/src/extension.ts index 8a95106..559e5fe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import * as path from "path"; import { FileParseStore } from "./state"; import { JavaFileWatcher } from "./JavaFileWatcher"; import { initializeParser } from "./parser"; -import { parseAndStore, ensureInitialized } from "./parser"; +import { parseAndStore } from "./parser"; import { minimatch } from "minimatch"; // This method is called when your extension is activated @@ -24,12 +24,16 @@ export async function activate(context: vscode.ExtensionContext) { //console.log('Congratulations, your extension "codescape" is now active!'); // sidebar view - const provider = new CodescapeViewProvider(context.extensionUri, javaWatcher); + const provider = new CodescapeViewProvider( + context.extensionUri, + javaWatcher, + store, + ); context.subscriptions.push( vscode.window.registerWebviewViewProvider("codescape.Cityview", provider), ); const create = vscode.commands.registerCommand("codescape.createPanel", () => - createPanel(context, javaWatcher), + createPanel(context, javaWatcher, store), ); // Parse all existing Java files on startup const existingFiles = await getJavaFiles(); @@ -102,9 +106,31 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(scan); } +function sendFullState(javaWatcher: JavaFileWatcher, store: FileParseStore) { + const snap = store.snapshot(); + + const classes = snap + .filter(({ entry }) => entry.status === "parsed") + .flatMap(({ entry }) => entry.data || []); + + const payload = { + classes, + status: classes.length === 0 ? "empty" : "ready", + }; + + console.log("[FULL_STATE] sending:", payload.classes.length, "classes"); + + javaWatcher.broadcast({ + type: "FULL_STATE", + payload, + }); +} + +console.log("CREATE PANEL CALLED"); function createPanel( context: vscode.ExtensionContext, javaWatcher: JavaFileWatcher, + store: FileParseStore, ) { const panel = vscode.window.createWebviewPanel( // internal ID @@ -126,23 +152,16 @@ function createPanel( //listen for messages FROM the webview panel.webview.onDidReceiveMessage((message) => { console.log("Received from webview:", message); - javaWatcher.addWebview(panel.webview); - }); - //send mock data TO the webview (Change this to run a full state change) - panel.webview.postMessage({ - type: "AST_DATA", - payload: { - files: [ - { - name: "App.tsx", - lines: 120, - functions: 4, - classes: 2, - }, - ], - }, + if (message.type === "READY") { + console.log("WEBVIEW READY RECEIVED"); + javaWatcher.addWebview(panel.webview); + + //send FULL_STATE when frontend is ready + sendFullState(javaWatcher, store); + } }); + panel.onDidDispose(() => { javaWatcher.removeWebview(panel.webview); }); @@ -232,6 +251,7 @@ class CodescapeViewProvider implements vscode.WebviewViewProvider { constructor( private extensionUri: vscode.Uri, private javaWatcher: JavaFileWatcher, + private store: FileParseStore, ) {} resolveWebviewView(webviewView: vscode.WebviewView) { webviewView.webview.options = { @@ -245,6 +265,14 @@ class CodescapeViewProvider implements vscode.WebviewViewProvider { this.extensionUri, ); this.javaWatcher.addWebview(webviewView.webview); + + webviewView.webview.onDidReceiveMessage((message) => { + if (message.type === "READY") { + console.log("SIDEBAR READY"); + sendFullState(this.javaWatcher, this.store); + } + }); + //ensure proper disposing webviewView.onDidDispose(() => this.javaWatcher.removeWebview(webviewView.webview), @@ -314,51 +342,47 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { let hoveredBuilding = null; //state update function that also triggers a re-render - function updateState(newData) { - console.log("update state called with data: ", newData); + //function updateState(newData) { + //console.log("update state called with data: ", newData); // store new parsed class data - state.classes = newData; + //state.classes = newData; // determine UI state - if (!newData) { + //if (!newData) { // null or undefined, something went wrong - state.status = "error"; - } else if (newData.length === 0) { + //state.status = "error"; + //} else if (newData.length === 0) { // valid array but no classes - state.status = "empty"; - } else { + //state.status = "empty"; + //} else { // valid array with classes - state.status = "ready"; - } + //state.status = "ready"; + //} // run layout before rendering - runAutoLayout(); + //runAutoLayout(); //assign the colors before re-rendering - assignColors(); + //assignColors(); // re-render canvas - render(); - } - - //will later integrate with arjuns logic? - function runAutoLayout() { - - //clear previous layout - state.layout = {}; + //render(); + //} - state.classes.forEach((cls, index) => { - - //simple layout for now (grid-based) - const col = 3 + index * 2; - const row = 3 + index; - - state.layout[cls.Classname] = { - col, - row - }; - }); - } + + //function runAutoLayout() { + //state.layout = {}; + //const cols = Math.ceil(Math.sqrt(state.classes.length)); // grid width + //state.classes.forEach((cls, index) => { + //const col = index % cols; + //const row = Math.floor(index / cols); + + //state.layout[cls.Classname] = { + //col: col + 3, + //row: row + 3 + //}; + //}); + //} function assignColors() { const newColorMap = {}; @@ -388,8 +412,70 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { state.colors = newColorMap; } - function getCanvasCoordinates(event) { + function patchState({ changed = [], related = [], removed = [] }) { + console.log("patchState called"); + + const nodes = buildNodesFromClasses(state.classes); + state.layout = computeLayout(nodes); + + //remove deleted classes + state.classes = state.classes.filter( + cls => !removed.includes(cls.Classname) + ); + //create a map for fast updates + const classMap = new Map( + state.classes.map(cls => [cls.Classname, cls]) + ); + + //apply changed + related updates + [...changed, ...related].forEach(cls => { + classMap.set(cls.Classname, cls); + }); + + //convert back to array + state.classes = Array.from(classMap.values()); + + //update UI state + if (state.classes.length === 0) { + state.status = "empty"; + } else { + state.status = "ready"; + } + + //runAutoLayout(); + assignColors(); + render(); + + } + + function buildNodesFromClasses(classes) { + const classNames = new Set(classes.map(c => c.Classname)); + + return classes.map(cls => { + const neighbors = []; + + //extract dependencies from fields + if (cls.Fields) { + cls.Fields.forEach(field => { + const type = field.type; + + //only include if it's another class in the project + if (classNames.has(type)) { + neighbors.push(type); + } + }); + } + + return { + id: cls.Classname, + name: cls.Classname, + neighbors + }; + }); + } + + function getCanvasCoordinates(event) { const rect = canvas.getBoundingClientRect(); return { @@ -482,7 +568,17 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { } // ready state -> render buildings - state.classes.forEach((cls) => { + const sortedClasses = [...state.classes].sort((a, b) => { + const posA = state.layout[a.Classname]; + const posB = state.layout[b.Classname]; + + if (!posA || !posB) return 0; + + // sort by depth (row + col) + return (posA.row + posA.col) - (posB.row + posB.col); + }); + + sortedClasses.forEach((cls) => { const position = state.layout[cls.Classname]; if (!position) return; @@ -564,40 +660,78 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { ctx.fillText("Error parsing files.", 50, 50); } + function computeLayout(nodes) { + const layout = {}; + let row = 0; + const placed = new Set(); + + for (const node of nodes) { + if (!placed.has(node.id)) { + layout[node.id] = { col: 0, row }; + placed.add(node.id); + + let col = 1; + for (const neighbor of node.neighbors) { + if (!placed.has(neighbor)) { + layout[neighbor] = { col, row }; + placed.add(neighbor); + col++; + } + } + row++; + } + } + + return layout; +} - // Listen for FULL_STATE (and legacy AST_DATA) from the extension - window.addEventListener('message', event => { - console.log('Message received:', event.data); - const msg = event.data; - if (msg.type === 'FULL_STATE' && msg.payload) { - fileData = msg.payload.files || []; - if (msg.payload.status === 'empty') { - // Frontend can show empty state; for now still call render() - } - if (msg.payload.errors && msg.payload.errors.length > 0) { - console.warn('Parse errors:', msg.payload.errors); - } - render(); - } else if (msg.type === 'AST_DATA' && msg.payload && msg.payload.files) { - fileData = msg.payload.files; - render(); - } else if (msg.type === 'PARTIAL_STATE' && msg.payload) { - //create default values because may not exist in payload - const { changed = [], related = [], removed = [] } = msg.payload; - console.log('[PARTIAL_STATE] changed:', changed.map(c => c.Classname)); - console.log('[PARTIAL_STATE] related:', related.map(c => c.Classname)); - console.log('[PARTIAL_STATE] removed:', removed); - // TODO: update individual buildings instead of full re-render - if(changed.length){ - updateState(changed); - } - if(related.length){ - updateState(related); + // Listen for FULL_STATE (and legacy AST_DATA) from the extension + window.addEventListener('message', event => { + console.log('Message received:', event.data); + const msg = event.data; - } + if (msg.type === 'FULL_STATE' && msg.payload) { + console.log("CLASSES:", msg.payload.classes); + console.log('[FULL_STATE] received:', msg.payload); - } - }); + state.classes = msg.payload.classes; + + //build graph input + const nodes = buildNodesFromClasses(state.classes); + + // run algorithm + state.layout = computeLayout(nodes); + + assignColors(); + render(); + + if (msg.payload.status === 'empty') { + console.log('Empty state'); + } + + if (msg.payload.errors && msg.payload.errors.length > 0) { + console.warn('Parse errors:', msg.payload.errors); + } + + return; // stop here + } + + else if (msg.type === 'AST_DATA' && msg.payload && msg.payload.files) { + console.log('[AST_DATA]'); + // you probably don't need this anymore, but leaving safe + return; + } + + else if (msg.type === 'PARTIAL_STATE' && msg.payload) { + const { changed = [], related = [], removed = [] } = msg.payload; + + console.log('[PARTIAL_STATE] changed:', changed.map(c => c.Classname)); + console.log('[PARTIAL_STATE] related:', related.map(c => c.Classname)); + console.log('[PARTIAL_STATE] removed:', removed); + + patchState(msg.payload); + } +}); window.addEventListener('resize', () => { canvas.width = window.innerWidth; From 8ad2221c409b36b6e0058ec76922097dc6074495 Mon Sep 17 00:00:00 2001 From: Arjun Sikka Date: Tue, 24 Mar 2026 18:34:52 -0400 Subject: [PATCH 02/15] Add Python city support and example workspaces --- README.md | 7 +- docs/USAGE.md | 23 +- examples/java-city/RouteMap.java | 11 + examples/java-city/ShuttleService.java | 11 + examples/java-city/TransitHub.java | 29 ++ examples/python-city/city_tools.py | 5 + examples/python-city/network.py | 15 + examples/python-city/route_map.py | 6 + package-lock.json | 5 - src/JavaFileWatcher.ts | 34 +- src/WebviewManager.ts | 348 ++------------- src/cityLayout.ts | 28 ++ src/cityWebviewHtml.ts | 362 ++++++++++++++++ src/extension.ts | 565 +++---------------------- src/parser/pythonExtractor.ts | 57 ++- src/relations.ts | 21 +- src/test/cityLayout.test.ts | 77 ++++ src/test/exampleWorkspaces.test.ts | 75 ++++ src/test/extension.test.ts | 129 +++++- src/test/fixtures/NestedClasses.py | 9 + src/test/pythonExtractor.test.ts | 12 + src/test/relations.test.ts | 13 + src/types/messages.ts | 108 ++--- 23 files changed, 982 insertions(+), 968 deletions(-) create mode 100644 examples/java-city/RouteMap.java create mode 100644 examples/java-city/ShuttleService.java create mode 100644 examples/java-city/TransitHub.java create mode 100644 examples/python-city/city_tools.py create mode 100644 examples/python-city/network.py create mode 100644 examples/python-city/route_map.py create mode 100644 src/cityLayout.ts create mode 100644 src/cityWebviewHtml.ts create mode 100644 src/test/cityLayout.test.ts create mode 100644 src/test/exampleWorkspaces.test.ts create mode 100644 src/test/fixtures/NestedClasses.py diff --git a/README.md b/README.md index f20498c..05eb39d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Codescape -Codescape is a VS Code extension that parses Java code and renders it as an isometric city. +Codescape is a VS Code extension that parses Java and Python code and renders it as an isometric city. - Buildings represent classes/interfaces. - Height is based on methods + fields. @@ -15,6 +15,7 @@ Active prototype with working parser, watcher, relationship graph, and canvas re | Area | Status | Notes | |---|---|---| | Java parsing (Tree-sitter) | Implemented | Classes/interfaces, methods, fields, constructors | +| Python parsing (Tree-sitter) | Implemented | Classes, module nodes, methods, fields, inheritance | | Relationship graph | Implemented | Extends/implements/field/ctor dependencies | | Incremental updates | Implemented | Watcher emits `PARTIAL_STATE` | | Explorer webview | Implemented | `codescape.Cityview` | @@ -43,6 +44,10 @@ npm run compile - Open the explorer view `Codescape City`, or - Run command `Create Panel` (`codescape.createPanel`). +Known-good example workspaces: +- `examples/java-city` +- `examples/python-city` + ## Development - Watch compile: diff --git a/docs/USAGE.md b/docs/USAGE.md index d8093f7..4840a32 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -5,7 +5,7 @@ ### Prerequisites - VS Code `^1.74.0` - Node.js + npm -- Java project/workspace with `.java` files +- Java and/or Python project/workspace with `.java` / `.py` files ### Build and run from source 1. Clone and enter repo. @@ -37,7 +37,7 @@ Placement options in VS Code: ## 3) What the Visualization Means -- Building = Java class/interface (`ClassInfo`). +- Building = parsed Java or Python entity (`ClassInfo`). - Building height = complexity proxy (`methods + fields`, min 1). - Building color = relationship signal (intended UX) and currently implemented as stable per-class palette assignment. - Related classes = computed in backend (`relations.ts`) for incremental updates. @@ -56,7 +56,7 @@ Important current behavior: ## 5) Incremental Updates -Codescape watches `**/*.java` and sends partial updates: +Codescape watches `**/*.java` and `**/*.py` and sends partial updates: - File changed: - Re-parse file. @@ -92,20 +92,29 @@ Runtime-registered internal commands (not contributed in `package.json`): ## 8) Practical Workflow 1. Start extension host (`F5`). -2. Open Java workspace in extension host window. +2. Open a Java or Python workspace in extension host window. 3. Open Codescape via sidebar or `Create Panel` command. -4. Edit/save Java files and observe updates. +4. Edit/save Java or Python files and observe updates. 5. Use zoom wheel to inspect layout. ## 9) Troubleshooting - City view not updating: - - Confirm `.java` file is not excluded by `.exclude`. + - Confirm the `.java` or `.py` file is not excluded by `.exclude`. - Check extension host logs for parser errors. - Empty view: - - Verify workspace has Java files. + - Verify workspace has Java or Python files. - Run `codescape.scan` (re-parses backend store; current implementation may still require reopening/reloading view to reflect a full-state refresh). - Commands missing: - Recompile (`npm run compile`) and relaunch extension host. + +## 10) Example Workspaces + +For a known-good visual smoke test, open one of these folders in the extension host: + +- `examples/java-city` +- `examples/python-city` + +Both are intentionally small so the generated city stays compact and in view with the current layout algorithm. diff --git a/examples/java-city/RouteMap.java b/examples/java-city/RouteMap.java new file mode 100644 index 0000000..17bc07b --- /dev/null +++ b/examples/java-city/RouteMap.java @@ -0,0 +1,11 @@ +public class RouteMap { + private int districtCode; + + public RouteMap(int districtCode) { + this.districtCode = districtCode; + } + + public int getDistrictCode() { + return districtCode; + } +} diff --git a/examples/java-city/ShuttleService.java b/examples/java-city/ShuttleService.java new file mode 100644 index 0000000..72766b3 --- /dev/null +++ b/examples/java-city/ShuttleService.java @@ -0,0 +1,11 @@ +public class ShuttleService { + private TransitHub hub; + + public ShuttleService(TransitHub hub) { + this.hub = hub; + } + + public TransitHub getHub() { + return hub; + } +} diff --git a/examples/java-city/TransitHub.java b/examples/java-city/TransitHub.java new file mode 100644 index 0000000..2655de6 --- /dev/null +++ b/examples/java-city/TransitHub.java @@ -0,0 +1,29 @@ +public class TransitHub { + private RouteMap routeMap; + private Gate primaryGate; + + public TransitHub(RouteMap routeMap, Gate primaryGate) { + this.routeMap = routeMap; + this.primaryGate = primaryGate; + } + + public RouteMap getRouteMap() { + return routeMap; + } + + public Gate getPrimaryGate() { + return primaryGate; + } + + static class Gate { + private int gateNumber; + + Gate(int gateNumber) { + this.gateNumber = gateNumber; + } + + public int getGateNumber() { + return gateNumber; + } + } +} diff --git a/examples/python-city/city_tools.py b/examples/python-city/city_tools.py new file mode 100644 index 0000000..37f5623 --- /dev/null +++ b/examples/python-city/city_tools.py @@ -0,0 +1,5 @@ +DEFAULT_BLOCKS = 12 + + +def estimate_blocks(distance): + return (distance + DEFAULT_BLOCKS - 1) // DEFAULT_BLOCKS diff --git a/examples/python-city/network.py b/examples/python-city/network.py new file mode 100644 index 0000000..e259cb1 --- /dev/null +++ b/examples/python-city/network.py @@ -0,0 +1,15 @@ +class BaseStation: + def __init__(self, district): + self.district = district + + def label(self): + return f"Station<{self.district}>" + + +class DispatchCenter(BaseStation): + def __init__(self, district): + super().__init__(district) + self.active_routes = 3 + + def assign_route(self, route_name): + return f"{route_name}:{self.active_routes}" diff --git a/examples/python-city/route_map.py b/examples/python-city/route_map.py new file mode 100644 index 0000000..1f6957a --- /dev/null +++ b/examples/python-city/route_map.py @@ -0,0 +1,6 @@ +class RouteMap: + def __init__(self): + self.routes = ["A", "B"] + + def count(self): + return len(self.routes) diff --git a/package-lock.json b/package-lock.json index 7b864c3..7754f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -452,7 +452,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -731,7 +730,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1260,7 +1258,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2977,7 +2974,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3068,7 +3064,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/JavaFileWatcher.ts b/src/JavaFileWatcher.ts index 40c7a07..2414f81 100644 --- a/src/JavaFileWatcher.ts +++ b/src/JavaFileWatcher.ts @@ -5,14 +5,11 @@ import { parseAndStore } from './parser'; import { ClassInfo } from './parser/javaExtractor'; import { buildGraph, getRelated } from './relations'; import { WebviewManager } from './WebviewManager'; - -type IncrementalChangePayload = { - changed?: ClassInfo[]; - related?: ClassInfo[]; - removed?: string[]; -}; +import { computeCityLayout } from './cityLayout'; +import type { PartialStatePayload } from './types/messages'; export class JavaFileWatcher { private _watcher: vscode.FileSystemWatcher; + private _pythonWatcher: vscode.FileSystemWatcher; constructor(store: FileParseStore, private webviewManager: WebviewManager) { this._watcher = vscode.workspace.createFileSystemWatcher('**/*.java'); @@ -27,7 +24,7 @@ export class JavaFileWatcher { const before = store.get(uri); const removedNames = (before?.data ?? []).map((c: ClassInfo) => c.Classname); store.remove(uri); - this.postIncrementalChange({ removed: removedNames }); + this.postIncrementalChange(this.buildPartialStatePayload([], removedNames, store)); }); // Python file watcher — same incremental pipeline as Java @@ -48,36 +45,37 @@ export class JavaFileWatcher { const before = store.get(uri); const removedNames = (before?.data ?? []).map((c: ClassInfo) => c.Classname); store.remove(uri); - if (this._webviews.length === 0) { - console.log("webviews not initialized yet"); - return; - } - this.postIncrementalChange({ removed: removedNames }, this._webviews); + this.postIncrementalChange(this.buildPartialStatePayload([], removedNames, store)); }); } private buildPartialStatePayload( changedClasses: ClassInfo[], removedNames: string[], store: FileParseStore - ): { changed: ClassInfo[]; related: ClassInfo[]; removed: string[] } { + ): PartialStatePayload { const allClasses = store.snapshot().flatMap(e => e.entry.data ?? []); + const layout = computeCityLayout(allClasses); const graph = buildGraph(allClasses); const changedNames = changedClasses.map(c => c.Classname); const relatedNames = getRelated([...changedNames, ...removedNames], graph); const relatedClasses = allClasses.filter(c => relatedNames.includes(c.Classname)); - return { changed: changedClasses, related: relatedClasses, removed: removedNames }; + return { + changed: changedClasses, + related: relatedClasses, + removed: removedNames, + fullClasses: allClasses, + layout, + }; } private async handleIncrementalChange(uri: vscode.Uri, store: FileParseStore) { if (!await isExcluded(uri)) { const { changed, removed } = await parseAndStore(uri, store); - //create payload from parsed data - const payload: IncrementalChangePayload = this.buildPartialStatePayload(changed, removed, store); - //send message to frontend + const payload = this.buildPartialStatePayload(changed, removed, store); this.postIncrementalChange(payload); } } - private async postIncrementalChange(payload: IncrementalChangePayload) { + private postIncrementalChange(payload: PartialStatePayload): void { this.webviewManager.broadcastPartialState(payload); } diff --git a/src/WebviewManager.ts b/src/WebviewManager.ts index 5092a3b..9379457 100644 --- a/src/WebviewManager.ts +++ b/src/WebviewManager.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; -import { ClassInfo } from './parser/javaExtractor'; +import { buildCityWebviewHtml } from './cityWebviewHtml'; +import type { FullStatePayload, PartialStatePayload } from './types/messages'; type ViewLocation = 'side' | 'bottom'; @@ -9,6 +10,8 @@ interface ManagedWebview { isReady: boolean; } +export type WebviewExtensionMessageHandler = (message: unknown) => void | Promise; + /** * WebviewManager handles creating, tracking, and syncing multiple webview panels * across different view locations (side pane, bottom panel). Ensures all active @@ -16,14 +19,19 @@ interface ManagedWebview { */ export class WebviewManager { private webviews: Map = new Map(); - private lastFullState: any = null; - private messageQueue: Array<{ type: string; payload: any }> = []; + private lastFullState: FullStatePayload | null = null; + private messageQueue: Array<{ type: string; payload: unknown }> = []; + + constructor( + private extensionUri: vscode.Uri, + private extensionMessageHandler?: WebviewExtensionMessageHandler, + ) { } - constructor(private extensionUri: vscode.Uri) { } + /** Latest FULL_STATE payload (for explorer sidebar sync on READY). */ + getLastFullState(): FullStatePayload | null { + return this.lastFullState; + } - /** - * Creates a new webview panel at the specified location - */ createWebview(location: ViewLocation): vscode.WebviewPanel { const viewColumn = location === 'side' ? vscode.ViewColumn.Two : vscode.ViewColumn.Nine; const title = location === 'side' ? 'Codescape Side' : 'Codescape Bottom'; @@ -50,12 +58,11 @@ export class WebviewManager { const viewId = this.generateViewId(); this.webviews.set(viewId, managedWebview); - // Listen for ready signal from webview - panel.webview.onDidReceiveMessage((message) => { - if (message.type === 'READY') { + panel.webview.onDidReceiveMessage(async (message: unknown) => { + const msg = message as { type?: string }; + if (msg.type === 'READY') { console.log(`Webview ready: ${viewId}`); managedWebview.isReady = true; - // Send full state immediately to new view if (this.lastFullState) { panel.webview.postMessage({ type: 'FULL_STATE', @@ -63,9 +70,11 @@ export class WebviewManager { }); } } + if (this.extensionMessageHandler) { + await this.extensionMessageHandler(message); + } }); - // Handle disposal panel.onDidDispose(() => { console.log(`Webview disposed: ${viewId}`); this.webviews.delete(viewId); @@ -74,10 +83,7 @@ export class WebviewManager { return panel; } - /** - * Broadcasts a FULL_STATE message to all active views - */ - broadcastFullState(state: any): void { + broadcastFullState(state: FullStatePayload): void { this.lastFullState = state; const message = { type: 'FULL_STATE', @@ -96,14 +102,7 @@ export class WebviewManager { } } - /** - * Broadcasts a PARTIAL_STATE (incremental changes) to all active views - */ - broadcastPartialState(payload: { - changed?: ClassInfo[]; - related?: ClassInfo[]; - removed?: string[]; - }): void { + broadcastPartialState(payload: PartialStatePayload): void { const message = { type: 'PARTIAL_STATE', payload, @@ -119,16 +118,10 @@ export class WebviewManager { } } - /** - * Get the number of active webview instances - */ getActiveViewCount(): number { return this.webviews.size; } - /** - * Check if a specific location has an active view - */ hasLocationActive(location: ViewLocation): boolean { for (const managed of this.webviews.values()) { if (managed.location === location) { @@ -138,9 +131,6 @@ export class WebviewManager { return false; } - /** - * Dispose all webviews - */ disposeAll(): void { for (const managed of this.webviews.values()) { managed.panel.dispose(); @@ -148,10 +138,6 @@ export class WebviewManager { this.webviews.clear(); } - /** - * Gets all registered webviews for external systems (like JavaFileWatcher) - * that need to send messages directly - */ getAllWebviews(): vscode.Webview[] { return Array.from(this.webviews.values()) .filter((m) => m.isReady) @@ -169,292 +155,6 @@ export class WebviewManager { const umlUri = webview.asWebviewUri( vscode.Uri.joinPath(this.extensionUri, 'src', 'webview', 'uml.js') ); - - return ` - - - - - - - - - - - - - `; + return buildCityWebviewHtml(rendererUri.toString(), umlUri.toString()); } } diff --git a/src/cityLayout.ts b/src/cityLayout.ts new file mode 100644 index 0000000..e1fcfc5 --- /dev/null +++ b/src/cityLayout.ts @@ -0,0 +1,28 @@ +import { ClassInfo } from './parser/javaExtractor'; +import { buildGraph } from './relations'; +import { BuildingNode, LayoutMap } from './layout/types'; +import { computeLayout } from './layout/placer'; + +/** + * Maps parsed ClassInfo entities to BuildingNode inputs for the shared auto-layout + * algorithm (neighbor grouping + inner-class depth). + */ +export function classInfosToBuildingNodes(classes: ClassInfo[]): BuildingNode[] { + const graph = buildGraph(classes); + return classes.map((cls) => { + const deps = graph.dependsOn.get(cls.Classname) ?? new Set(); + return { + id: cls.Classname, + name: cls.Classname, + neighbors: Array.from(deps), + parentClass: cls.parentClass, + innerClasses: cls.innerClasses, + }; + }); +} + +/** Computes grid layout for the full class list (Java + Python + module nodes). */ +export function computeCityLayout(classes: ClassInfo[]): LayoutMap { + const nodes = classInfosToBuildingNodes(classes); + return computeLayout(nodes); +} diff --git a/src/cityWebviewHtml.ts b/src/cityWebviewHtml.ts new file mode 100644 index 0000000..70e5853 --- /dev/null +++ b/src/cityWebviewHtml.ts @@ -0,0 +1,362 @@ +/** + * Shared city canvas webview document (WebviewManager + explorer sidebar). + * Keep in sync with extension ↔ webview message payloads (FULL_STATE / PARTIAL_STATE). + */ +export function buildCityWebviewHtml(rendererUri: string, umlUri: string): string { + return ` + + + + + + + + + + + + + `; +} diff --git a/src/extension.ts b/src/extension.ts index 6377fa8..09afd16 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,15 +6,22 @@ import { FileParseStore } from "./state"; import { JavaFileWatcher } from "./JavaFileWatcher"; import { WebviewManager } from "./WebviewManager"; import { initializeParser } from "./parser"; -import { parseAndStore, ensureInitialized } from './parser'; +import { parseAndStore } from './parser'; import { minimatch } from 'minimatch'; +import { computeCityLayout } from './cityLayout'; +import { buildCityWebviewHtml } from './cityWebviewHtml'; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export async function activate(context: vscode.ExtensionContext) { console.log("CODESCAPE ACTIVATED"); const store = new FileParseStore(); - const webviewManager = new WebviewManager(context.extensionUri); + const webviewManager = new WebviewManager(context.extensionUri, async (message: unknown) => { + const msg = message as { type?: string; payload?: { className?: string } }; + if (msg.type === 'OPEN_CLASS_SOURCE' && msg.payload?.className) { + await openClassSourceFromClassName(msg.payload.className, store); + } + }); const scan = vscode.commands.registerCommand('codescape.scan', () => workspaceScan(store, webviewManager)); const javaWatcher = new JavaFileWatcher(store, webviewManager); await initializeParser(); @@ -24,7 +31,7 @@ export async function activate(context: vscode.ExtensionContext) { //console.log('Congratulations, your extension "codescape" is now active!'); // sidebar view - const provider = new CodescapeViewProvider(context.extensionUri, webviewManager); + const provider = new CodescapeViewProvider(context.extensionUri, webviewManager, store); context.subscriptions.push( vscode.window.registerWebviewViewProvider('codescape.Cityview', provider) ); @@ -40,12 +47,9 @@ export async function activate(context: vscode.ExtensionContext) { console.log('Created bottom panel webview'); }); - // Legacy create panel command (just create side panel) const create = vscode.commands.registerCommand('codescape.createPanel', () => { webviewManager.createWebview('side'); }); - - const create = vscode.commands.registerCommand('codescape.createPanel', () => createPanel(context, javaWatcher, store)); // Parse all existing Java and Python files on startup const existingFiles = [ ...await getJavaFiles(), @@ -56,10 +60,11 @@ export async function activate(context: vscode.ExtensionContext) { await parseAndStore(uri, store); } - // Send full state to webview manager after initial parse + const classes = store.snapshot().flatMap(e => e.entry.data ?? []); const fullState = { - classes: store.snapshot().flatMap(e => e.entry.data ?? []), - status: 'ready' + classes, + layout: computeCityLayout(classes), + status: classes.length > 0 ? 'ready' as const : 'empty' as const, }; webviewManager.broadcastFullState(fullState); @@ -138,10 +143,14 @@ async function openClassSourceFromClassName(className: string, store: FileParseS const snapshot = store.snapshot(); for (const { uri, entry } of snapshot) { - if (entry.status !== 'parsed' || !entry.data) continue; + if (entry.status !== 'parsed' || !entry.data) { + continue; + } const match = entry.data.find(c => c.Classname === className); - if (!match) continue; + if (!match) { + continue; + } const fileUri = vscode.Uri.parse(uri); @@ -182,102 +191,7 @@ async function openClassSourceFromClassName(className: string, store: FileParseS vscode.window.showInformationMessage(`Could not find source for class ${className}.`); } -function createPanel(context : vscode.ExtensionContext, javaWatcher : JavaFileWatcher, store: FileParseStore){ - - const panel = vscode.window.createWebviewPanel( - // internal ID - 'codescapeWebview', - // title shown to user - 'Codescape', - vscode.ViewColumn.One, - { - // lets the webview run JavaScript - enableScripts: true, - localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'src', 'webview')] - } - ); - - // html content for the web viewer - panel.webview.html = getWebviewContent(panel.webview, context.extensionUri); - //listen for messages FROM the webview - panel.webview.onDidReceiveMessage(async (message: any) => { - console.log('Received from webview:', message); - if (message.type === 'EXPORT_HTML') { - const htmlContent = generateStandaloneHtml(message.payload.fileData); - const uri = await vscode.window.showSaveDialog({ - filters: { 'HTML': ['html'] }, - defaultUri: vscode.Uri.file('codescape-city.html') - }); - if (uri) { - await vscode.workspace.fs.writeFile(uri, Buffer.from(htmlContent)); - vscode.window.showInformationMessage('City exported as HTML!'); - } - } - if (message.type === 'OPEN_CLASS_SOURCE' && message.payload?.className) { - await openClassSourceFromClassName(message.payload.className, store); - } - if (message.type === 'EXPORT_JSON') { - const uri = await vscode.window.showSaveDialog({ - filters: { 'JSON': ['json'] }, - defaultUri: vscode.Uri.file('codescape-city.json') - }); - if (uri) { - await vscode.workspace.fs.writeFile( - uri, - Buffer.from(JSON.stringify(message.payload, null, 2)) - ); - vscode.window.showInformationMessage('City state exported as JSON!'); - } - } - }); - - function generateStandaloneHtml(fileData: any[]): string { - // Read the JS files and inline them - return ` - - - - Codescape City - - - - - - - - `; - } - - //send mock data TO the webview - javaWatcher.addWebview(panel.webview); - - //send mock data TO the webview (Change this to run a full state change) - panel.webview.postMessage({ - type: 'AST_DATA', - payload: { - files: [ - { - name: 'App.tsx', - lines: 120, - functions: 4, - classes: 2 - } - ] - } - }); - panel.onDidDispose( () =>{javaWatcher.removeWebview(panel.webview)}); -} - -async function workspaceScan(store: FileParseStore) { +async function workspaceScan(store: FileParseStore, webviewManager: WebviewManager) { // Get all supported source files not in exclude const files = [ ...await getJavaFiles(), @@ -305,10 +219,11 @@ async function workspaceScan(store: FileParseStore) { console.log(`Workspace scan complete. Parsed ${successCount} files, ${failureCount} failures. Store has ${snap.length} entries.`); vscode.window.showInformationMessage(`Codescape: Scan complete! Successfully parsed ${successCount} files (${failureCount} failures).`); - // Broadcast updated full state to all webviews + const scannedClasses = snap.flatMap(e => e.entry.data ?? []); const fullState = { - classes: snap.flatMap(e => e.entry.data ?? []), - status: successCount > 0 ? 'ready' : 'empty' + classes: scannedClasses, + layout: computeCityLayout(scannedClasses), + status: successCount > 0 && scannedClasses.length > 0 ? ('ready' as const) : ('empty' as const), }; webviewManager.broadcastFullState(fullState); } @@ -374,422 +289,42 @@ export async function isExcluded(uri: vscode.Uri): Promise { return excludeFiles.some((pattern) => minimatch(path, pattern)); } -// sidebar view -class CodescapeViewProvider implements vscode.WebviewViewProvider { - //add WebviewManager to sidebar - constructor(private extensionUri: vscode.Uri, private webviewManager: WebviewManager) { } - resolveWebviewView(webviewView: vscode.WebviewView) { - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'src', 'webview')] - }; - webviewView.webview.html = getWebviewContent(webviewView.webview, this.extensionUri); - // Note: WebviewView is managed separately by VS Code, not by WebviewManager - } -} - -// new canvas-based city visualization that renders an isometric grid and buildings from AST data -function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { +function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string { const rendererUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "src", "webview", "renderer.js"), + vscode.Uri.joinPath(extensionUri, 'src', 'webview', 'renderer.js'), ); const umlUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "src", "webview", "uml.js"), + vscode.Uri.joinPath(extensionUri, 'src', 'webview', 'uml.js'), ); + return buildCityWebviewHtml(rendererUri.toString(), umlUri.toString()); +} - return ` - - - - - - - - - - - - - `; } // This method is called when your extension is deactivated diff --git a/src/parser/pythonExtractor.ts b/src/parser/pythonExtractor.ts index 90d8419..64c7bdb 100644 --- a/src/parser/pythonExtractor.ts +++ b/src/parser/pythonExtractor.ts @@ -53,16 +53,17 @@ export function extractPythonEntities(source: string, moduleName: string): Class } const results: ClassInfo[] = []; + const classMap = new Map(); const root = tree.rootNode; - // Top-level class definitions (including decorated classes) + // Top-level class definitions (including decorated); nested classes recurse into body for (const child of root.namedChildren) { if (child.type === 'class_definition') { - results.push(buildClassInfo(child)); + visitPythonClass(child, null, results, classMap); } else if (child.type === 'decorated_definition') { const inner = child.namedChildren.find((c: SyntaxNode) => c.type === 'class_definition'); if (inner) { - results.push(buildClassInfo(inner)); + visitPythonClass(inner, null, results, classMap); } } } @@ -71,16 +72,58 @@ export function extractPythonEntities(source: string, moduleName: string): Class const moduleEntry = buildModuleEntry(root, moduleName); if (moduleEntry) { results.push(moduleEntry); + classMap.set(moduleEntry.Classname, moduleEntry); } + linkInnerClasses(results, classMap); return results; } +/** Walk nested class_definition nodes and set parentClass like Java extractor. */ +function visitPythonClass( + node: SyntaxNode, + parentClassName: string | null, + results: ClassInfo[], + classMap: Map +): void { + const info = buildClassInfo(node, parentClassName); + results.push(info); + classMap.set(info.Classname, info); + + const body = node.childForFieldName('body'); + if (!body) { return; } + + for (const child of body.namedChildren) { + if (child.type === 'class_definition') { + visitPythonClass(child, info.Classname, results, classMap); + } else if (child.type === 'decorated_definition') { + const inner = child.namedChildren.find((c: SyntaxNode) => c.type === 'class_definition'); + if (inner) { + visitPythonClass(inner, info.Classname, results, classMap); + } + } + } +} + +function linkInnerClasses(results: ClassInfo[], classMap: Map): void { + for (const classInfo of results) { + if (classInfo.parentClass) { + const parent = classMap.get(classInfo.parentClass); + if (parent) { + if (!parent.innerClasses) { + parent.innerClasses = []; + } + parent.innerClasses.push(classInfo.Classname); + } + } + } +} + // --------------------------------------------------------------------------- // Class extraction // --------------------------------------------------------------------------- -function buildClassInfo(node: SyntaxNode): ClassInfo { +function buildClassInfo(node: SyntaxNode, parentClassName: string | null = null): ClassInfo { const name = node.childForFieldName('name')?.text ?? 'Unknown'; const loc = node.endPosition.row - node.startPosition.row + 1; @@ -97,7 +140,7 @@ function buildClassInfo(node: SyntaxNode): ClassInfo { const fields = extractFields(body, initNode); const constructors = initNode ? [buildConstructorInfo(initNode)] : []; - return { + const classInfo: ClassInfo = { Classname: name, Methods: methods, Loc: loc, @@ -107,6 +150,10 @@ function buildClassInfo(node: SyntaxNode): ClassInfo { Fields: fields, Constructors: constructors, }; + if (parentClassName) { + classInfo.parentClass = parentClassName; + } + return classInfo; } function extractBaseClasses(superclassesNode: SyntaxNode | null): string[] { diff --git a/src/relations.ts b/src/relations.ts index f7d1f81..4f320bb 100644 --- a/src/relations.ts +++ b/src/relations.ts @@ -14,7 +14,7 @@ export interface ClassGraph { // Collects all class names that a single class depends on. // Draws from: Extends, Implements, field types, constructor parameter types, and parent/inner class relationships. -function getDependencies(cls: ClassInfo): Set { +function getDependencies(cls: ClassInfo, classByName: Map): Set { const deps = new Set(); if (cls.Extends && !PRIMITIVES.has(cls.Extends)) { @@ -38,17 +38,19 @@ function getDependencies(cls: ClassInfo): Set { } } - // Add dependencies for inner/nested class relationships - // Inner classes depend on their parent class + // Inner / nested classes depend on their parent if (cls.parentClass && !PRIMITIVES.has(cls.parentClass)) { deps.add(cls.parentClass); } - // Inner classes depend on sibling classes in the same parent - if (cls.parentClass && cls.innerClasses) { - for (const innerClass of cls.innerClasses) { - if (!PRIMITIVES.has(innerClass)) { - deps.add(innerClass); + // Sibling inner/nested classes under the same parent (metadata lives on parent) + if (cls.parentClass) { + const parent = classByName.get(cls.parentClass); + if (parent?.innerClasses) { + for (const sibling of parent.innerClasses) { + if (sibling !== cls.Classname && !PRIMITIVES.has(sibling)) { + deps.add(sibling); + } } } } @@ -61,6 +63,7 @@ function getDependencies(cls: ClassInfo): Set { export function buildGraph(allClasses: ClassInfo[]): ClassGraph { const dependsOn = new Map>(); const dependedOnBy = new Map>(); + const classByName = new Map(allClasses.map((c) => [c.Classname, c])); // Initialise nodes for every known class for (const cls of allClasses) { @@ -70,7 +73,7 @@ export function buildGraph(allClasses: ClassInfo[]): ClassGraph { // Populate edges for (const cls of allClasses) { - const deps = getDependencies(cls); + const deps = getDependencies(cls, classByName); dependsOn.set(cls.Classname, deps); for (const dep of deps) { diff --git a/src/test/cityLayout.test.ts b/src/test/cityLayout.test.ts new file mode 100644 index 0000000..369b6f0 --- /dev/null +++ b/src/test/cityLayout.test.ts @@ -0,0 +1,77 @@ +import * as assert from 'assert'; +import { computeCityLayout, classInfosToBuildingNodes } from '../cityLayout'; +import { ClassInfo } from '../parser/javaExtractor'; + +suite('City layout bridging', () => { + test('computeCityLayout produces positions for Java-like and Python-like entities', () => { + const classes: ClassInfo[] = [ + { + Classname: 'A', + Methods: [], + Loc: 1, + Type: 'public', + Extends: null, + Implements: [], + Fields: [], + Constructors: [], + }, + { + Classname: 'B', + Methods: [], + Loc: 1, + Type: 'class', + Extends: 'A', + Implements: [], + Fields: [], + Constructors: [], + }, + { + Classname: '', + Methods: [{ name: 'f', parameters: [], returnType: 'None', modifiers: [] }], + Loc: 5, + Type: 'module', + Extends: null, + Implements: [], + Fields: [], + Constructors: [], + }, + ]; + const layout = computeCityLayout(classes); + assert.ok(layout['A'], 'layout should include A'); + assert.ok(layout['B'], 'layout should include B'); + assert.ok(layout[''], 'layout should include module node'); + }); + + test('nested classes get parentClass edges in building graph', () => { + const classes: ClassInfo[] = [ + { + Classname: 'Outer', + Methods: [], + Loc: 1, + Type: 'class', + Extends: null, + Implements: [], + Fields: [], + Constructors: [], + innerClasses: ['Inner'], + }, + { + Classname: 'Inner', + Methods: [], + Loc: 1, + Type: 'class', + Extends: null, + Implements: [], + Fields: [], + Constructors: [], + parentClass: 'Outer', + }, + ]; + const nodes = classInfosToBuildingNodes(classes); + const inner = nodes.find((n) => n.id === 'Inner'); + assert.ok(inner); + assert.ok(inner!.neighbors.includes('Outer')); + const layout = computeCityLayout(classes); + assert.strictEqual(layout['Inner'].depth, 1); + }); +}); diff --git a/src/test/exampleWorkspaces.test.ts b/src/test/exampleWorkspaces.test.ts new file mode 100644 index 0000000..6d9ba26 --- /dev/null +++ b/src/test/exampleWorkspaces.test.ts @@ -0,0 +1,75 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { computeCityLayout } from '../cityLayout'; +import { LayoutMap } from '../layout/types'; +import { initParser, extractClasses, ClassInfo } from '../parser/javaExtractor'; +import { initPythonParser, extractPythonEntities } from '../parser/pythonExtractor'; + +const examplesDir = path.join(__dirname, '..', '..', 'examples'); + +function loadExampleEntities(exampleFolder: string): ClassInfo[] { + const folder = path.join(examplesDir, exampleFolder); + const fileNames = fs.readdirSync(folder).sort(); + const entities: ClassInfo[] = []; + + for (const fileName of fileNames) { + const fullPath = path.join(folder, fileName); + const source = fs.readFileSync(fullPath, 'utf8'); + + if (fileName.endsWith('.java')) { + entities.push(...extractClasses(source)); + continue; + } + + if (fileName.endsWith('.py')) { + entities.push(...extractPythonEntities(source, path.basename(fileName, '.py'))); + } + } + + return entities; +} + +function maxCoordinate( + layout: LayoutMap, + axis: 'col' | 'row' | 'depth' +): number { + return Math.max( + ...Object.values(layout).map((entry) => (axis === 'depth' ? entry.depth ?? 0 : entry[axis])) + ); +} + +suite('Example Workspaces', () => { + suiteSetup(async () => { + await initParser(); + await initPythonParser(); + }); + + test('java example workspace parses into a compact city layout', () => { + const entities = loadExampleEntities('java-city'); + const layout = computeCityLayout(entities); + + assert.deepStrictEqual( + entities.map((entity) => entity.Classname).sort(), + ['Gate', 'RouteMap', 'ShuttleService', 'TransitHub'] + ); + assert.strictEqual(Object.keys(layout).length, entities.length); + assert.ok(maxCoordinate(layout, 'col') <= 2, 'java example should stay within three columns'); + assert.ok(maxCoordinate(layout, 'row') <= 1, 'java example should stay within two rows'); + assert.ok(maxCoordinate(layout, 'depth') <= 1, 'java example should have at most one nested layer'); + }); + + test('python example workspace parses into a compact city layout', () => { + const entities = loadExampleEntities('python-city'); + const layout = computeCityLayout(entities); + + assert.deepStrictEqual( + entities.map((entity) => entity.Classname).sort(), + ['', 'BaseStation', 'DispatchCenter', 'RouteMap'] + ); + assert.strictEqual(Object.keys(layout).length, entities.length); + assert.ok(maxCoordinate(layout, 'col') <= 1, 'python example should stay within two columns'); + assert.ok(maxCoordinate(layout, 'row') <= 3, 'python example should stay within four rows'); + assert.ok(maxCoordinate(layout, 'depth') === 0, 'python example should remain top-level'); + }); +}); diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 4ca0ab4..3649494 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,15 +1,126 @@ import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it +import * as fs from 'fs'; +import * as path from 'path'; import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; +import { WebviewManager } from '../WebviewManager'; +import { initParser, extractClasses, ClassInfo } from '../parser/javaExtractor'; +import { initPythonParser, extractPythonEntities } from '../parser/pythonExtractor'; +import { computeCityLayout } from '../cityLayout'; + +const fixturesDir = path.join(__dirname, '..', '..', 'src', 'test', 'fixtures'); + +type PostedMessage = { + type: string; + payload: { + classes: ClassInfo[]; + layout: Record; + status: 'ready' | 'empty' | 'loading'; + }; +}; + +type MessageHandler = (message: unknown) => void | Promise; + +function loadEntitiesFromFixtures(): ClassInfo[] { + const fixtureFiles = fs.readdirSync(fixturesDir).filter((name) => name.endsWith('.java') || name.endsWith('.py')); + const entities: ClassInfo[] = []; + + for (const fileName of fixtureFiles) { + const fullPath = path.join(fixturesDir, fileName); + const source = fs.readFileSync(fullPath, 'utf8'); + + if (fileName.endsWith('.java')) { + entities.push(...extractClasses(source)); + continue; + } + + const moduleName = path.basename(fileName, '.py'); + entities.push(...extractPythonEntities(source, moduleName)); + } + + return entities; +} + +function createFakePanelSink() { + const postedMessages: PostedMessage[] = []; + let messageHandler: MessageHandler | undefined; + + const webview = { + html: '', + onDidReceiveMessage: (handler: MessageHandler) => { + messageHandler = handler; + return new vscode.Disposable(() => { + messageHandler = undefined; + }); + }, + postMessage: async (message: PostedMessage) => { + postedMessages.push(message); + return true; + }, + asWebviewUri: (uri: vscode.Uri) => uri, + }; + + const panel = { + webview, + onDidDispose: () => new vscode.Disposable(() => undefined), + dispose: () => undefined, + }; + + return { + panel, + postedMessages, + async sendToExtension(message: unknown) { + if (!messageHandler) { + throw new Error('Webview message handler not registered'); + } + await messageHandler(message); + }, + }; +} suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); + suiteSetup(async () => { + await initParser(); + await initPythonParser(); + }); + + test('webview receives a non-empty city state for real workspace fixtures', async () => { + const originalCreateWebviewPanel = vscode.window.createWebviewPanel; + const fakeSink = createFakePanelSink(); + + Object.defineProperty(vscode.window, 'createWebviewPanel', { + configurable: true, + value: () => fakeSink.panel, + }); + + try { + const manager = new WebviewManager(vscode.Uri.file(process.cwd())); + manager.createWebview('side'); + + const classes = loadEntitiesFromFixtures(); + const payload = { + classes, + layout: computeCityLayout(classes), + status: classes.length > 0 ? ('ready' as const) : ('empty' as const), + }; + + manager.broadcastFullState(payload); + await fakeSink.sendToExtension({ type: 'READY' }); + + assert.ok(fakeSink.postedMessages.length > 0, 'webview should receive a message after READY'); - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); + const fullState = fakeSink.postedMessages.find((message) => message.type === 'FULL_STATE'); + assert.ok(fullState, 'expected a FULL_STATE message'); + assert.ok(fullState!.payload.classes.length > 0, 'FULL_STATE should contain parsed entities'); + assert.ok( + Object.keys(fullState!.payload.layout).length > 0, + 'FULL_STATE should contain building layout positions' + ); + assert.strictEqual(fullState!.payload.status, 'ready'); + } finally { + Object.defineProperty(vscode.window, 'createWebviewPanel', { + configurable: true, + value: originalCreateWebviewPanel, + }); + } + }); }); diff --git a/src/test/fixtures/NestedClasses.py b/src/test/fixtures/NestedClasses.py new file mode 100644 index 0000000..ab5076c --- /dev/null +++ b/src/test/fixtures/NestedClasses.py @@ -0,0 +1,9 @@ +class Outer: + """Outer class with nested inner.""" + + class Inner: + def inner_method(self) -> int: + return 1 + + def outer_method(self) -> None: + pass diff --git a/src/test/pythonExtractor.test.ts b/src/test/pythonExtractor.test.ts index 33e2e64..3271ce7 100644 --- a/src/test/pythonExtractor.test.ts +++ b/src/test/pythonExtractor.test.ts @@ -94,6 +94,18 @@ suite('Python Extractor Tests', () => { assert.ok(area.modifiers.some((d: string) => d.includes('abstractmethod'))); }); + test('extracts nested classes with parentClass and innerClasses', () => { + const source = loadFixture('NestedClasses.py'); + const result = extractPythonEntities(source, 'NestedClasses'); + + const outer = result.find((r) => r.Classname === 'Outer'); + const inner = result.find((r) => r.Classname === 'Inner'); + assert.ok(outer, 'Outer should exist'); + assert.ok(inner, 'Inner should exist'); + assert.strictEqual(inner!.parentClass, 'Outer'); + assert.deepStrictEqual(outer!.innerClasses, ['Inner']); + }); + test('extracts module-level entry with standalone functions and imports', () => { const source = loadFixture('ModuleLevel.py'); const result = extractPythonEntities(source, 'ModuleLevel'); diff --git a/src/test/relations.test.ts b/src/test/relations.test.ts index b82f6ce..cb84a61 100644 --- a/src/test/relations.test.ts +++ b/src/test/relations.test.ts @@ -117,4 +117,17 @@ suite('Relations Graph', () => { assert.ok(graph.dependsOn.get('Foo')!.has('List')); }); + test('sibling inner/nested classes link via parent innerClasses metadata', () => { + const parent: ClassInfo = { + ...cls('Outer'), + innerClasses: ['A', 'B'], + }; + const a: ClassInfo = { ...cls('A'), parentClass: 'Outer' }; + const b: ClassInfo = { ...cls('B'), parentClass: 'Outer' }; + const graph = buildGraph([parent, a, b]); + assert.ok(graph.dependsOn.get('A')!.has('Outer')); + assert.ok(graph.dependsOn.get('A')!.has('B')); + assert.ok(graph.dependsOn.get('B')!.has('A')); + }); + }); diff --git a/src/types/messages.ts b/src/types/messages.ts index d70dd93..e9f29dd 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -1,24 +1,15 @@ /** * Message contract for extension ↔ webview communication. - * All messages from backend → webview are part of the WebviewMessage union. + * Shapes match runtime payloads (ClassInfo from parsers, layout from cityLayout). */ -/** Per-class/interface data extracted from a Java file (matches parser output). */ -export interface ParsedClassInfo { - Classname: string; - Methods: string[]; - Loc: number; - Type: string; - Extends: string | null; - Implements: string[]; - // Inner/nested class support - parentClass?: string; // Name of parent class (if this is an inner class) - innerClasses?: string[]; // Names of inner classes (if this class contains any) - isStatic?: boolean; // Whether this is a static inner class - isAnonymous?: boolean; // Whether this is an anonymous class -} +import type { ClassInfo } from '../parser/javaExtractor'; +import type { LayoutMap } from '../layout/types'; + +/** Per-class entity as produced by Java/Python extractors (single shared schema). */ +export type ParsedClassInfo = ClassInfo; -/** Single parsed file entry in FULL_STATE or PARTIAL_STATE changed list. */ +/** Single parsed file entry (optional grouping; webview may use flat `classes` only). */ export interface ParsedFile { path: string; classes: ParsedClassInfo[]; @@ -30,71 +21,39 @@ export interface ParseErrorEntry { message: string; } +/** Payload for FULL_STATE — flat class list + precomputed layout from extension. */ +export interface FullStatePayload { + classes: ClassInfo[]; + layout: LayoutMap; + status: 'ready' | 'empty' | 'loading'; + rootPath?: string; + timestamp?: string; + errors?: ParseErrorEntry[]; + /** Legacy: file-grouped data */ + files?: ParsedFile[]; +} + /** - * Full state message — entire parsed codebase. Sent on initial load. - * - * @example - * { - * type: "FULL_STATE", - * payload: { - * files: [ - * { - * path: "/workspace/src/Main.java", - * classes: [ - * { - * Classname: "Main", - * Methods: ["main(String[])"], - * Loc: 10, - * Type: "public", - * Extends: null, - * Implements: [] - * } - * ] - * } - * ], - * rootPath: "/workspace", - * timestamp: "2025-02-18T12:00:00.000Z", - * status: "ok", - * errors: [] - * } - * } + * Partial/incremental update. Always includes `fullClasses` + `layout` after an incremental + * parse so the webview can replace state without dropping unrelated classes. */ +export interface PartialStatePayload { + changed: ClassInfo[]; + related: ClassInfo[]; + removed: string[]; + fullClasses: ClassInfo[]; + layout: LayoutMap; + timestamp?: string; +} + export interface FullStateMessage { type: 'FULL_STATE'; - payload: { - files: ParsedFile[]; - rootPath?: string; - timestamp: string; - status: 'ok' | 'empty'; - errors: ParseErrorEntry[]; - }; + payload: FullStatePayload; } -/** - * Partial state message — only what changed. Sent on incremental updates. - * - * @example - * { - * type: "PARTIAL_STATE", - * payload: { - * changed: [ - * { - * path: "/workspace/src/Updated.java", - * classes: [{ Classname: "Updated", Methods: [], Loc: 5, Type: "public", Extends: null, Implements: [] }] - * } - * ], - * removed: ["/workspace/src/Deleted.java"], - * timestamp: "2025-02-18T12:05:00.000Z" - * } - * } - */ export interface PartialStateMessage { type: 'PARTIAL_STATE'; - payload: { - changed: ParsedFile[]; - removed: string[]; - timestamp: string; - }; + payload: PartialStatePayload; } /** All message types from backend → webview. */ @@ -105,11 +64,10 @@ export interface ReadyMessage { type: 'READY'; } -/** Message from webview → extension: user clicked a building and wants to open its class source. */ +/** Message from webview → extension: user clicked a building and wants to open its source. */ export interface OpenClassSourceMessage { type: 'OPEN_CLASS_SOURCE'; payload: { - /** Simple identifier; extension maps this back to a file via the parse store. */ className: string; }; } From bf195bca5e3c5c28b7155da40d366c1f38e65102 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Wed, 25 Mar 2026 14:39:01 -0400 Subject: [PATCH 03/15] integrated webview manager to work with webviewview alongside panel. Added functionality to add webview containers (webviewview/webviewpanel) to the manager so it could work with webviewview provider. Fixed syntax errors in rendering html --- src/JavaFileWatcher.ts | 15 +- src/WebviewManager.ts | 363 ++++------------------------------------- src/extension.ts | 216 ++++++++++++------------ 3 files changed, 154 insertions(+), 440 deletions(-) diff --git a/src/JavaFileWatcher.ts b/src/JavaFileWatcher.ts index 40c7a07..a20d0da 100644 --- a/src/JavaFileWatcher.ts +++ b/src/JavaFileWatcher.ts @@ -12,17 +12,18 @@ type IncrementalChangePayload = { removed?: string[]; }; export class JavaFileWatcher { - private _watcher: vscode.FileSystemWatcher; + private _javaWatcher: vscode.FileSystemWatcher; + private _pythonWatcher : vscode.FileSystemWatcher; constructor(store: FileParseStore, private webviewManager: WebviewManager) { - this._watcher = vscode.workspace.createFileSystemWatcher('**/*.java'); + this._javaWatcher = vscode.workspace.createFileSystemWatcher('**/*.java'); - this._watcher.onDidChange(async (uri: vscode.Uri) => { + this._javaWatcher.onDidChange(async (uri: vscode.Uri) => { console.log('Java file changed:', uri.fsPath); this.handleIncrementalChange(uri, store); }); - this._watcher.onDidDelete((uri: vscode.Uri) => { + this._javaWatcher.onDidDelete((uri: vscode.Uri) => { console.log('Java file deleted:', uri.fsPath); const before = store.get(uri); const removedNames = (before?.data ?? []).map((c: ClassInfo) => c.Classname); @@ -48,11 +49,11 @@ export class JavaFileWatcher { const before = store.get(uri); const removedNames = (before?.data ?? []).map((c: ClassInfo) => c.Classname); store.remove(uri); - if (this._webviews.length === 0) { + if (webviewManager.getActiveViewCount() === 0) { console.log("webviews not initialized yet"); return; } - this.postIncrementalChange({ removed: removedNames }, this._webviews); + this.postIncrementalChange({ removed: removedNames }); }); } private buildPartialStatePayload( @@ -82,7 +83,7 @@ export class JavaFileWatcher { } dispose() { - this._watcher.dispose(); + this._javaWatcher.dispose(); this._pythonWatcher.dispose(); } } \ No newline at end of file diff --git a/src/WebviewManager.ts b/src/WebviewManager.ts index 2215af8..e26f129 100644 --- a/src/WebviewManager.ts +++ b/src/WebviewManager.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode'; import { ClassInfo } from './parser/javaExtractor'; +import {getWebviewContent} from './extension' -type ViewLocation = 'side' | 'bottom'; +type ViewLocation = 'side' | 'bottom'; +type WebviewContainer = vscode.WebviewView | vscode.WebviewPanel interface ManagedWebview { - panel: vscode.WebviewPanel; - location: ViewLocation; + container: WebviewContainer; isReady: boolean; } @@ -24,7 +25,7 @@ export class WebviewManager { /** * Creates a new webview panel at the specified location */ - createWebview(location: ViewLocation): vscode.WebviewPanel { + createPanel(location: ViewLocation): vscode.WebviewPanel { const viewColumn = location === 'side' ? vscode.ViewColumn.Two : vscode.ViewColumn.Nine; const title = location === 'side' ? 'Codescape Side' : 'Codescape Bottom'; @@ -39,25 +40,37 @@ export class WebviewManager { } ); - panel.webview.html = this.getWebviewContent(panel.webview); + panel.webview.html = getWebviewContent(panel.webview, this.extensionUri); + this.addWebview(panel) + return panel; + } + addWebview(container: WebviewContainer){ + const managedWebview: ManagedWebview = { - panel, - location, + container : container, isReady: false, }; const viewId = this.generateViewId(); this.webviews.set(viewId, managedWebview); - - // Listen for ready signal from webview - panel.webview.onDidReceiveMessage((message) => { + // WebviewView is already ready when provider gives it to us + if ('onDidChangeVisibility' in container) { + managedWebview.isReady = true; + if (this.lastFullState) { + container.webview.postMessage({ + type: 'FULL_STATE', + payload: this.lastFullState, + }); + } + } + container.webview.onDidReceiveMessage((message) => { if (message.type === 'READY') { console.log(`Webview ready: ${viewId}`); managedWebview.isReady = true; // Send full state immediately to new view if (this.lastFullState) { - panel.webview.postMessage({ + container.webview.postMessage({ type: 'FULL_STATE', payload: this.lastFullState, }); @@ -65,13 +78,12 @@ export class WebviewManager { } }); - // Handle disposal - panel.onDidDispose(() => { + // Handle disposal + container.onDidDispose(() => { console.log(`Webview disposed: ${viewId}`); this.webviews.delete(viewId); }); - return panel; } /** @@ -87,7 +99,7 @@ export class WebviewManager { for (const [viewId, managed] of this.webviews) { if (managed.isReady) { console.log(`Broadcasting FULL_STATE to ${viewId}`); - managed.panel.webview + managed.container.webview .postMessage(message) .then((delivered) => console.log(`FULL_STATE delivered to ${viewId}: ${delivered}`)); } else { @@ -108,11 +120,11 @@ export class WebviewManager { type: 'PARTIAL_STATE', payload, }; - + console.log("Broadcasting starting, sending to: " + this.getActiveViewCount() + " Panels") for (const [viewId, managed] of this.webviews) { if (managed.isReady) { console.log(`Broadcasting PARTIAL_STATE to ${viewId}`); - managed.panel.webview + managed.container.webview .postMessage(message) .then((delivered) => console.log(`PARTIAL_STATE delivered to ${viewId}: ${delivered}`)); } @@ -126,26 +138,18 @@ export class WebviewManager { return this.webviews.size; } - /** - * Check if a specific location has an active view - */ - hasLocationActive(location: ViewLocation): boolean { - for (const managed of this.webviews.values()) { - if (managed.location === location) { - return true; - } - } - return false; - } /** * Dispose all webviews */ disposeAll(): void { - for (const managed of this.webviews.values()) { - managed.panel.dispose(); + for (const id of this.webviews.keys()) { + let managed = this.webviews.get(id); + if(managed != null && 'dispose' in managed.container){ + managed.container.dispose(); + this.webviews.delete(id); } - this.webviews.clear(); + } } /** @@ -155,305 +159,10 @@ export class WebviewManager { getAllWebviews(): vscode.Webview[] { return Array.from(this.webviews.values()) .filter((m) => m.isReady) - .map((m) => m.panel.webview); + .map((m) => m.container.webview); } private generateViewId(): string { return `view_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } - - private getWebviewContent(webview: vscode.Webview): string { - const rendererUri = webview.asWebviewUri( - vscode.Uri.joinPath(this.extensionUri, 'src', 'webview', 'renderer.js') - ); - const umlUri = webview.asWebviewUri( - vscode.Uri.joinPath(this.extensionUri, 'src', 'webview', 'uml.js') - ); - - return ` - - - - - - - - - - - - - `; - } } diff --git a/src/extension.ts b/src/extension.ts index 955eb9b..622ce3d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,24 +28,24 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.window.registerWebviewViewProvider("codescape.Cityview", provider), ); - const create = vscode.commands.registerCommand("codescape.createPanel", () => - createPanel(context, javaWatcher), - ); + // const create = vscode.commands.registerCommand("codescape.createPanel", () => + // createPanel(context, javaWatcher, store), + // ); // Register multi-view commands const createSidePanel = vscode.commands.registerCommand('codescape.createSidePanel', () => { - const panel = webviewManager.createWebview('side'); + const panel = webviewManager.createPanel('side'); console.log('Created side panel webview'); }); const createBottomPanel = vscode.commands.registerCommand('codescape.createBottomPanel', () => { - const panel = webviewManager.createWebview('bottom'); + const panel = webviewManager.createPanel('bottom'); console.log('Created bottom panel webview'); }); // Legacy create panel command (just create side panel) const create = vscode.commands.registerCommand('codescape.createPanel', () => { - webviewManager.createWebview('side'); + webviewManager.createPanel('side'); }); // Parse all existing Java and Python files on startup @@ -184,104 +184,104 @@ async function openClassSourceFromClassName(className: string, store: FileParseS vscode.window.showInformationMessage(`Could not find source for class ${className}.`); } -function createPanel(context : vscode.ExtensionContext, javaWatcher : JavaFileWatcher, store: FileParseStore){ +// function createPanel(context : vscode.ExtensionContext, javaWatcher : JavaFileWatcher, store: FileParseStore){ - const panel = vscode.window.createWebviewPanel( - // internal ID - "codescapeWebview", - // title shown to user - "Codescape", - vscode.ViewColumn.One, - { - // lets the webview run JavaScript - enableScripts: true, - localResourceRoots: [ - vscode.Uri.joinPath(context.extensionUri, "src", "webview"), - ], - }, - ); - - // html content for the web viewer - panel.webview.html = getWebviewContent(panel.webview, context.extensionUri); - //listen for messages FROM the webview - panel.webview.onDidReceiveMessage(async (message: any) => { - console.log('Received from webview:', message); - if (message.type === 'EXPORT_HTML') { - const htmlContent = generateStandaloneHtml(message.payload.fileData); - const uri = await vscode.window.showSaveDialog({ - filters: { 'HTML': ['html'] }, - defaultUri: vscode.Uri.file('codescape-city.html') - }); - if (uri) { - await vscode.workspace.fs.writeFile(uri, Buffer.from(htmlContent)); - vscode.window.showInformationMessage('City exported as HTML!'); - } - } - if (message.type === 'OPEN_CLASS_SOURCE' && message.payload?.className) { - await openClassSourceFromClassName(message.payload.className, store); - } - if (message.type === 'EXPORT_JSON') { - const uri = await vscode.window.showSaveDialog({ - filters: { 'JSON': ['json'] }, - defaultUri: vscode.Uri.file('codescape-city.json') - }); - if (uri) { - await vscode.workspace.fs.writeFile( - uri, - Buffer.from(JSON.stringify(message.payload, null, 2)) - ); - vscode.window.showInformationMessage('City state exported as JSON!'); - } - } - }); - - function generateStandaloneHtml(fileData: any[]): string { - // Read the JS files and inline them - return ` - - - - Codescape City - - - - - - - - `; - } - - //send mock data TO the webview - javaWatcher.addWebview(panel.webview); +// const panel = vscode.window.createWebviewPanel( +// // internal ID +// "codescapeWebview", +// // title shown to user +// "Codescape", +// vscode.ViewColumn.One, +// { +// // lets the webview run JavaScript +// enableScripts: true, +// localResourceRoots: [ +// vscode.Uri.joinPath(context.extensionUri, "src", "webview"), +// ], +// }, +// ); + +// // html content for the web viewer +// panel.webview.html = getWebviewContent(panel.webview, context.extensionUri); +// //listen for messages FROM the webview +// panel.webview.onDidReceiveMessage(async (message: any) => { +// console.log('Received from webview:', message); +// if (message.type === 'EXPORT_HTML') { +// const htmlContent = generateStandaloneHtml(message.payload.fileData); +// const uri = await vscode.window.showSaveDialog({ +// filters: { 'HTML': ['html'] }, +// defaultUri: vscode.Uri.file('codescape-city.html') +// }); +// if (uri) { +// await vscode.workspace.fs.writeFile(uri, Buffer.from(htmlContent)); +// vscode.window.showInformationMessage('City exported as HTML!'); +// } +// } +// if (message.type === 'OPEN_CLASS_SOURCE' && message.payload?.className) { +// await openClassSourceFromClassName(message.payload.className, store); +// } +// if (message.type === 'EXPORT_JSON') { +// const uri = await vscode.window.showSaveDialog({ +// filters: { 'JSON': ['json'] }, +// defaultUri: vscode.Uri.file('codescape-city.json') +// }); +// if (uri) { +// await vscode.workspace.fs.writeFile( +// uri, +// Buffer.from(JSON.stringify(message.payload, null, 2)) +// ); +// vscode.window.showInformationMessage('City state exported as JSON!'); +// } +// } +// }); + +// function generateStandaloneHtml(fileData: any[]): string { +// // Read the JS files and inline them +// return ` +// +// +// +// Codescape City +// +// +// +// +// +// +// +// `; +// } + +// //send mock data TO the webview +// // javaWatcher.addWebview(panel.webview); - //send mock data TO the webview (Change this to run a full state change) - panel.webview.postMessage({ - type: "AST_DATA", - payload: { - files: [ - { - name: "App.tsx", - lines: 120, - functions: 4, - classes: 2, - }, - ], - }, - }); - panel.onDidDispose(() => { - javaWatcher.removeWebview(panel.webview); - }); -} +// //send mock data TO the webview (Change this to run a full state change) +// panel.webview.postMessage({ +// type: "AST_DATA", +// payload: { +// files: [ +// { +// name: "App.tsx", +// lines: 120, +// functions: 4, +// classes: 2, +// }, +// ], +// }, +// }); +// panel.onDidDispose(() => { +// // javaWatcher.removeWebview(panel.webview); +// }); +// } async function workspaceScan(store: FileParseStore, webviewManager: WebviewManager) { // Get all supported source files not in exclude @@ -385,17 +385,20 @@ class CodescapeViewProvider implements vscode.WebviewViewProvider { //add WebviewManager to sidebar constructor(private extensionUri: vscode.Uri, private webviewManager: WebviewManager) { } resolveWebviewView(webviewView: vscode.WebviewView) { + console.log('resolveWebviewView called, view id:', webviewView.viewType); + webviewView.webview.options = { enableScripts: true, localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'src', 'webview')] }; webviewView.webview.html = getWebviewContent(webviewView.webview, this.extensionUri); // Note: WebviewView is managed separately by VS Code, not by WebviewManager + this.webviewManager.addWebview(webviewView); } } // new canvas-based city visualization that renders an isometric grid and buildings from AST data -function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { +export function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { const rendererUri = webview.asWebviewUri( vscode.Uri.joinPath(extensionUri, "src", "webview", "renderer.js"), ); @@ -420,7 +423,6 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { const vscode = acquireVsCodeApi(); const canvas = document.getElementById('cityCanvas'); const ctx = canvas.getContext('2d'); - canvas.width = window.innerWidth; canvas.height = window.innerHeight; @@ -571,7 +573,8 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { // Registry of rendered buildings for hit detection (hover/click). // Each entry is tracked in canvas/world coordinates before zoom. - const buildingRegistry = []; + //NOTE: THIS STOPS RENDER FROM RUNNING + //const buildingRegistry = []; //now only reads from state @@ -691,7 +694,7 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { } ); } - } + // restore canvas transform ctx.restore(); } @@ -755,6 +758,7 @@ function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { fileData = msg.payload.files; render(); } else if (msg.type === 'PARTIAL_STATE' && msg.payload) { + console.log("PARTIAL STATE CHANGE RECIEVE") //create default values because may not exist in payload const { changed = [], related = [], removed = [] } = msg.payload; console.log('[PARTIAL_STATE] changed:', changed.map(c => c.Classname)); From 9984b05e141822c2c06f87f5951e41843757ee58 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Wed, 25 Mar 2026 18:10:32 -0400 Subject: [PATCH 04/15] removed unecessary code --- src/extension.ts | 102 ----------------------------------------------- 1 file changed, 102 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 622ce3d..38a9b2f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,9 +28,6 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.window.registerWebviewViewProvider("codescape.Cityview", provider), ); - // const create = vscode.commands.registerCommand("codescape.createPanel", () => - // createPanel(context, javaWatcher, store), - // ); // Register multi-view commands const createSidePanel = vscode.commands.registerCommand('codescape.createSidePanel', () => { @@ -184,105 +181,6 @@ async function openClassSourceFromClassName(className: string, store: FileParseS vscode.window.showInformationMessage(`Could not find source for class ${className}.`); } -// function createPanel(context : vscode.ExtensionContext, javaWatcher : JavaFileWatcher, store: FileParseStore){ - -// const panel = vscode.window.createWebviewPanel( -// // internal ID -// "codescapeWebview", -// // title shown to user -// "Codescape", -// vscode.ViewColumn.One, -// { -// // lets the webview run JavaScript -// enableScripts: true, -// localResourceRoots: [ -// vscode.Uri.joinPath(context.extensionUri, "src", "webview"), -// ], -// }, -// ); - -// // html content for the web viewer -// panel.webview.html = getWebviewContent(panel.webview, context.extensionUri); -// //listen for messages FROM the webview -// panel.webview.onDidReceiveMessage(async (message: any) => { -// console.log('Received from webview:', message); -// if (message.type === 'EXPORT_HTML') { -// const htmlContent = generateStandaloneHtml(message.payload.fileData); -// const uri = await vscode.window.showSaveDialog({ -// filters: { 'HTML': ['html'] }, -// defaultUri: vscode.Uri.file('codescape-city.html') -// }); -// if (uri) { -// await vscode.workspace.fs.writeFile(uri, Buffer.from(htmlContent)); -// vscode.window.showInformationMessage('City exported as HTML!'); -// } -// } -// if (message.type === 'OPEN_CLASS_SOURCE' && message.payload?.className) { -// await openClassSourceFromClassName(message.payload.className, store); -// } -// if (message.type === 'EXPORT_JSON') { -// const uri = await vscode.window.showSaveDialog({ -// filters: { 'JSON': ['json'] }, -// defaultUri: vscode.Uri.file('codescape-city.json') -// }); -// if (uri) { -// await vscode.workspace.fs.writeFile( -// uri, -// Buffer.from(JSON.stringify(message.payload, null, 2)) -// ); -// vscode.window.showInformationMessage('City state exported as JSON!'); -// } -// } -// }); - -// function generateStandaloneHtml(fileData: any[]): string { -// // Read the JS files and inline them -// return ` -// -// -// -// Codescape City -// -// -// -// -// -// -// -// `; -// } - -// //send mock data TO the webview -// // javaWatcher.addWebview(panel.webview); - -// //send mock data TO the webview (Change this to run a full state change) -// panel.webview.postMessage({ -// type: "AST_DATA", -// payload: { -// files: [ -// { -// name: "App.tsx", -// lines: 120, -// functions: 4, -// classes: 2, -// }, -// ], -// }, -// }); -// panel.onDidDispose(() => { -// // javaWatcher.removeWebview(panel.webview); -// }); -// } - async function workspaceScan(store: FileParseStore, webviewManager: WebviewManager) { // Get all supported source files not in exclude const files = [ From e01166d1f1d544146c3d0b13cd8a76492d0e03fc Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Wed, 25 Mar 2026 18:24:58 -0400 Subject: [PATCH 05/15] Update src/extension.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 38a9b2f..4c5e67f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -656,7 +656,7 @@ export function getWebviewContent(webview: vscode.Webview, extensionUri: vscode. fileData = msg.payload.files; render(); } else if (msg.type === 'PARTIAL_STATE' && msg.payload) { - console.log("PARTIAL STATE CHANGE RECIEVE") + console.log("PARTIAL STATE CHANGE RECEIVE") //create default values because may not exist in payload const { changed = [], related = [], removed = [] } = msg.payload; console.log('[PARTIAL_STATE] changed:', changed.map(c => c.Classname)); From 15676128cf3d08d92e799236d3474d23414e1df5 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Wed, 25 Mar 2026 18:25:18 -0400 Subject: [PATCH 06/15] Update src/extension.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 4c5e67f..707b72b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -290,7 +290,7 @@ class CodescapeViewProvider implements vscode.WebviewViewProvider { localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'src', 'webview')] }; webviewView.webview.html = getWebviewContent(webviewView.webview, this.extensionUri); - // Note: WebviewView is managed separately by VS Code, not by WebviewManager + // Register this WebviewView with WebviewManager so it participates in the shared messaging/management logic this.webviewManager.addWebview(webviewView); } } From 7e8628ed5f878f4ff0196212e0479c4af79a3327 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Thu, 26 Mar 2026 19:21:10 -0400 Subject: [PATCH 07/15] organized files into extension, web, and media(shared rendering code). Created rudamentary CI pipeline for edits in media and tested rendering code on a local host website --- .github/workflows/ci.yml | 24 +++++++++++++++++++ .../.vscode-test.mjs | 0 .../eslint.config.mjs | 0 .../package-lock.json | 5 ---- package.json => extension/package.json | 0 {src => extension/src}/JavaFileWatcher.ts | 0 {src => extension/src}/WebviewManager.ts | 0 {src => extension/src}/extension.ts | 6 ++--- {src => extension/src}/layout/demo.ts | 0 {src => extension/src}/layout/placer.ts | 0 {src => extension/src}/layout/types.ts | 0 {src => extension/src}/parser.ts | 0 .../src}/parser/javaExtractor.ts | 0 .../src}/parser/pythonExtractor.ts | 0 {src => extension/src}/relations.ts | 0 {src => extension/src}/state.ts | 0 {src => extension/src}/test/extension.test.ts | 0 .../src}/test/fixtures/AbstractClass.py | 0 .../src}/test/fixtures/AbstractMethods.java | 0 .../src}/test/fixtures/AbstractService.java | 0 .../src}/test/fixtures/AsyncDecorated.py | 0 .../src}/test/fixtures/DeepNestedClasses.java | 0 .../test/fixtures/GenericBaseAndModuleCode.py | 0 .../src}/test/fixtures/Inheritance.py | 0 .../src}/test/fixtures/InnerClasses.java | 0 .../fixtures/InterfaceWithInnerTypes.java | 0 .../src}/test/fixtures/MethodOverloads.java | 0 .../src}/test/fixtures/MinimalClass.java | 0 .../src}/test/fixtures/ModuleLevel.py | 0 .../src}/test/fixtures/MultiClass.java | 0 .../src}/test/fixtures/NestedAssignments.py | 0 .../src}/test/fixtures/PersonClass.java | 0 .../src}/test/fixtures/Printable.java | 0 .../src}/test/fixtures/SimpleClass.java | 0 .../src}/test/fixtures/SimpleClass.py | 0 .../src}/test/fixtures/StaticMethods.java | 0 .../test/fixtures/VisibilityModifiers.java | 0 {src => extension/src}/test/layout.test.ts | 0 {src => extension/src}/test/parser.test.ts | 0 .../src}/test/pythonExtractor.test.ts | 0 {src => extension/src}/test/relations.test.ts | 0 {src => extension/src}/test/watcher.test.ts | 0 {src => extension/src}/types/messages.ts | 0 {src => extension/src}/webview/renderer.js | 0 {src => extension/src}/webview/uml.js | 0 tsconfig.json => extension/tsconfig.json | 0 web/index.html | 13 ++++++++++ 47 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml rename .vscode-test.mjs => extension/.vscode-test.mjs (100%) rename eslint.config.mjs => extension/eslint.config.mjs (100%) rename package-lock.json => extension/package-lock.json (99%) rename package.json => extension/package.json (100%) rename {src => extension/src}/JavaFileWatcher.ts (100%) rename {src => extension/src}/WebviewManager.ts (100%) rename {src => extension/src}/extension.ts (99%) rename {src => extension/src}/layout/demo.ts (100%) rename {src => extension/src}/layout/placer.ts (100%) rename {src => extension/src}/layout/types.ts (100%) rename {src => extension/src}/parser.ts (100%) rename {src => extension/src}/parser/javaExtractor.ts (100%) rename {src => extension/src}/parser/pythonExtractor.ts (100%) rename {src => extension/src}/relations.ts (100%) rename {src => extension/src}/state.ts (100%) rename {src => extension/src}/test/extension.test.ts (100%) rename {src => extension/src}/test/fixtures/AbstractClass.py (100%) rename {src => extension/src}/test/fixtures/AbstractMethods.java (100%) rename {src => extension/src}/test/fixtures/AbstractService.java (100%) rename {src => extension/src}/test/fixtures/AsyncDecorated.py (100%) rename {src => extension/src}/test/fixtures/DeepNestedClasses.java (100%) rename {src => extension/src}/test/fixtures/GenericBaseAndModuleCode.py (100%) rename {src => extension/src}/test/fixtures/Inheritance.py (100%) rename {src => extension/src}/test/fixtures/InnerClasses.java (100%) rename {src => extension/src}/test/fixtures/InterfaceWithInnerTypes.java (100%) rename {src => extension/src}/test/fixtures/MethodOverloads.java (100%) rename {src => extension/src}/test/fixtures/MinimalClass.java (100%) rename {src => extension/src}/test/fixtures/ModuleLevel.py (100%) rename {src => extension/src}/test/fixtures/MultiClass.java (100%) rename {src => extension/src}/test/fixtures/NestedAssignments.py (100%) rename {src => extension/src}/test/fixtures/PersonClass.java (100%) rename {src => extension/src}/test/fixtures/Printable.java (100%) rename {src => extension/src}/test/fixtures/SimpleClass.java (100%) rename {src => extension/src}/test/fixtures/SimpleClass.py (100%) rename {src => extension/src}/test/fixtures/StaticMethods.java (100%) rename {src => extension/src}/test/fixtures/VisibilityModifiers.java (100%) rename {src => extension/src}/test/layout.test.ts (100%) rename {src => extension/src}/test/parser.test.ts (100%) rename {src => extension/src}/test/pythonExtractor.test.ts (100%) rename {src => extension/src}/test/relations.test.ts (100%) rename {src => extension/src}/test/watcher.test.ts (100%) rename {src => extension/src}/types/messages.ts (100%) rename {src => extension/src}/webview/renderer.js (100%) rename {src => extension/src}/webview/uml.js (100%) rename tsconfig.json => extension/tsconfig.json (100%) create mode 100644 web/index.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7002bae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [main] + paths: + - 'media/**' + pull_request: + branches: [main] + paths: + - 'media/**' + +jobs: + extension: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install extension deps + run: cd extension && npm install + - name: Compile extension + run: cd extension && npm run compile \ No newline at end of file diff --git a/.vscode-test.mjs b/extension/.vscode-test.mjs similarity index 100% rename from .vscode-test.mjs rename to extension/.vscode-test.mjs diff --git a/eslint.config.mjs b/extension/eslint.config.mjs similarity index 100% rename from eslint.config.mjs rename to extension/eslint.config.mjs diff --git a/package-lock.json b/extension/package-lock.json similarity index 99% rename from package-lock.json rename to extension/package-lock.json index 7b864c3..7754f6b 100644 --- a/package-lock.json +++ b/extension/package-lock.json @@ -452,7 +452,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -731,7 +730,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1260,7 +1258,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2977,7 +2974,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3068,7 +3064,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/extension/package.json similarity index 100% rename from package.json rename to extension/package.json diff --git a/src/JavaFileWatcher.ts b/extension/src/JavaFileWatcher.ts similarity index 100% rename from src/JavaFileWatcher.ts rename to extension/src/JavaFileWatcher.ts diff --git a/src/WebviewManager.ts b/extension/src/WebviewManager.ts similarity index 100% rename from src/WebviewManager.ts rename to extension/src/WebviewManager.ts diff --git a/src/extension.ts b/extension/src/extension.ts similarity index 99% rename from src/extension.ts rename to extension/src/extension.ts index f30eda5..be2d19c 100644 --- a/src/extension.ts +++ b/extension/src/extension.ts @@ -35,16 +35,16 @@ context.subscriptions.push( // multi panels const createSidePanel = vscode.commands.registerCommand('codescape.createSidePanel', () => { - webviewManager.createWebview('side'); + webviewManager.createPanel('side'); }); const createBottomPanel = vscode.commands.registerCommand('codescape.createBottomPanel', () => { - webviewManager.createWebview('bottom'); + webviewManager.createPanel('bottom'); }); // legacy command const create = vscode.commands.registerCommand('codescape.createPanel', () => { - webviewManager.createWebview('side'); + webviewManager.createPanel('side'); }); // Parse all existing Java and Python files on startup diff --git a/src/layout/demo.ts b/extension/src/layout/demo.ts similarity index 100% rename from src/layout/demo.ts rename to extension/src/layout/demo.ts diff --git a/src/layout/placer.ts b/extension/src/layout/placer.ts similarity index 100% rename from src/layout/placer.ts rename to extension/src/layout/placer.ts diff --git a/src/layout/types.ts b/extension/src/layout/types.ts similarity index 100% rename from src/layout/types.ts rename to extension/src/layout/types.ts diff --git a/src/parser.ts b/extension/src/parser.ts similarity index 100% rename from src/parser.ts rename to extension/src/parser.ts diff --git a/src/parser/javaExtractor.ts b/extension/src/parser/javaExtractor.ts similarity index 100% rename from src/parser/javaExtractor.ts rename to extension/src/parser/javaExtractor.ts diff --git a/src/parser/pythonExtractor.ts b/extension/src/parser/pythonExtractor.ts similarity index 100% rename from src/parser/pythonExtractor.ts rename to extension/src/parser/pythonExtractor.ts diff --git a/src/relations.ts b/extension/src/relations.ts similarity index 100% rename from src/relations.ts rename to extension/src/relations.ts diff --git a/src/state.ts b/extension/src/state.ts similarity index 100% rename from src/state.ts rename to extension/src/state.ts diff --git a/src/test/extension.test.ts b/extension/src/test/extension.test.ts similarity index 100% rename from src/test/extension.test.ts rename to extension/src/test/extension.test.ts diff --git a/src/test/fixtures/AbstractClass.py b/extension/src/test/fixtures/AbstractClass.py similarity index 100% rename from src/test/fixtures/AbstractClass.py rename to extension/src/test/fixtures/AbstractClass.py diff --git a/src/test/fixtures/AbstractMethods.java b/extension/src/test/fixtures/AbstractMethods.java similarity index 100% rename from src/test/fixtures/AbstractMethods.java rename to extension/src/test/fixtures/AbstractMethods.java diff --git a/src/test/fixtures/AbstractService.java b/extension/src/test/fixtures/AbstractService.java similarity index 100% rename from src/test/fixtures/AbstractService.java rename to extension/src/test/fixtures/AbstractService.java diff --git a/src/test/fixtures/AsyncDecorated.py b/extension/src/test/fixtures/AsyncDecorated.py similarity index 100% rename from src/test/fixtures/AsyncDecorated.py rename to extension/src/test/fixtures/AsyncDecorated.py diff --git a/src/test/fixtures/DeepNestedClasses.java b/extension/src/test/fixtures/DeepNestedClasses.java similarity index 100% rename from src/test/fixtures/DeepNestedClasses.java rename to extension/src/test/fixtures/DeepNestedClasses.java diff --git a/src/test/fixtures/GenericBaseAndModuleCode.py b/extension/src/test/fixtures/GenericBaseAndModuleCode.py similarity index 100% rename from src/test/fixtures/GenericBaseAndModuleCode.py rename to extension/src/test/fixtures/GenericBaseAndModuleCode.py diff --git a/src/test/fixtures/Inheritance.py b/extension/src/test/fixtures/Inheritance.py similarity index 100% rename from src/test/fixtures/Inheritance.py rename to extension/src/test/fixtures/Inheritance.py diff --git a/src/test/fixtures/InnerClasses.java b/extension/src/test/fixtures/InnerClasses.java similarity index 100% rename from src/test/fixtures/InnerClasses.java rename to extension/src/test/fixtures/InnerClasses.java diff --git a/src/test/fixtures/InterfaceWithInnerTypes.java b/extension/src/test/fixtures/InterfaceWithInnerTypes.java similarity index 100% rename from src/test/fixtures/InterfaceWithInnerTypes.java rename to extension/src/test/fixtures/InterfaceWithInnerTypes.java diff --git a/src/test/fixtures/MethodOverloads.java b/extension/src/test/fixtures/MethodOverloads.java similarity index 100% rename from src/test/fixtures/MethodOverloads.java rename to extension/src/test/fixtures/MethodOverloads.java diff --git a/src/test/fixtures/MinimalClass.java b/extension/src/test/fixtures/MinimalClass.java similarity index 100% rename from src/test/fixtures/MinimalClass.java rename to extension/src/test/fixtures/MinimalClass.java diff --git a/src/test/fixtures/ModuleLevel.py b/extension/src/test/fixtures/ModuleLevel.py similarity index 100% rename from src/test/fixtures/ModuleLevel.py rename to extension/src/test/fixtures/ModuleLevel.py diff --git a/src/test/fixtures/MultiClass.java b/extension/src/test/fixtures/MultiClass.java similarity index 100% rename from src/test/fixtures/MultiClass.java rename to extension/src/test/fixtures/MultiClass.java diff --git a/src/test/fixtures/NestedAssignments.py b/extension/src/test/fixtures/NestedAssignments.py similarity index 100% rename from src/test/fixtures/NestedAssignments.py rename to extension/src/test/fixtures/NestedAssignments.py diff --git a/src/test/fixtures/PersonClass.java b/extension/src/test/fixtures/PersonClass.java similarity index 100% rename from src/test/fixtures/PersonClass.java rename to extension/src/test/fixtures/PersonClass.java diff --git a/src/test/fixtures/Printable.java b/extension/src/test/fixtures/Printable.java similarity index 100% rename from src/test/fixtures/Printable.java rename to extension/src/test/fixtures/Printable.java diff --git a/src/test/fixtures/SimpleClass.java b/extension/src/test/fixtures/SimpleClass.java similarity index 100% rename from src/test/fixtures/SimpleClass.java rename to extension/src/test/fixtures/SimpleClass.java diff --git a/src/test/fixtures/SimpleClass.py b/extension/src/test/fixtures/SimpleClass.py similarity index 100% rename from src/test/fixtures/SimpleClass.py rename to extension/src/test/fixtures/SimpleClass.py diff --git a/src/test/fixtures/StaticMethods.java b/extension/src/test/fixtures/StaticMethods.java similarity index 100% rename from src/test/fixtures/StaticMethods.java rename to extension/src/test/fixtures/StaticMethods.java diff --git a/src/test/fixtures/VisibilityModifiers.java b/extension/src/test/fixtures/VisibilityModifiers.java similarity index 100% rename from src/test/fixtures/VisibilityModifiers.java rename to extension/src/test/fixtures/VisibilityModifiers.java diff --git a/src/test/layout.test.ts b/extension/src/test/layout.test.ts similarity index 100% rename from src/test/layout.test.ts rename to extension/src/test/layout.test.ts diff --git a/src/test/parser.test.ts b/extension/src/test/parser.test.ts similarity index 100% rename from src/test/parser.test.ts rename to extension/src/test/parser.test.ts diff --git a/src/test/pythonExtractor.test.ts b/extension/src/test/pythonExtractor.test.ts similarity index 100% rename from src/test/pythonExtractor.test.ts rename to extension/src/test/pythonExtractor.test.ts diff --git a/src/test/relations.test.ts b/extension/src/test/relations.test.ts similarity index 100% rename from src/test/relations.test.ts rename to extension/src/test/relations.test.ts diff --git a/src/test/watcher.test.ts b/extension/src/test/watcher.test.ts similarity index 100% rename from src/test/watcher.test.ts rename to extension/src/test/watcher.test.ts diff --git a/src/types/messages.ts b/extension/src/types/messages.ts similarity index 100% rename from src/types/messages.ts rename to extension/src/types/messages.ts diff --git a/src/webview/renderer.js b/extension/src/webview/renderer.js similarity index 100% rename from src/webview/renderer.js rename to extension/src/webview/renderer.js diff --git a/src/webview/uml.js b/extension/src/webview/uml.js similarity index 100% rename from src/webview/uml.js rename to extension/src/webview/uml.js diff --git a/tsconfig.json b/extension/tsconfig.json similarity index 100% rename from tsconfig.json rename to extension/tsconfig.json diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..4a48161 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file From e03ad76b07904ab624ded49acfc92f21c4cb44a3 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Tue, 31 Mar 2026 15:01:47 -0400 Subject: [PATCH 08/15] implemented placer in javascript to allow for compatability with webview, fixed settings and launch to work with restructured folders --- .vscode/launch.json | 4 +-- extension/src/extension.ts | 74 +++++++++++++++----------------------- 2 files changed, 30 insertions(+), 48 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8880465..4b8df3f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,10 +10,10 @@ "type": "extensionHost", "request": "launch", "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" + "--extensionDevelopmentPath=${workspaceFolder}/extension" ], "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "${workspaceFolder}/extension/out/**/*.js" ], "preLaunchTask": "${defaultBuildTask}" } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index be2d19c..db88405 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -305,6 +305,7 @@ export function getWebviewContent(webview: vscode.Webview, extensionUri: vscode. const umlUri = webview.asWebviewUri( vscode.Uri.joinPath(extensionUri, "src", "webview", "uml.js"), ); + const layoutUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri,"src","webview","placer.js")); return ` @@ -319,6 +320,7 @@ export function getWebviewContent(webview: vscode.Webview, extensionUri: vscode. + + + + + + + `; +} \ No newline at end of file diff --git a/extension/src/extension.ts b/extension/src/extension.ts index db88405..ef35feb 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode"; import * as path from "path"; import { FileParseStore } from "./state"; import { JavaFileWatcher } from "./JavaFileWatcher"; -import { WebviewManager } from "./WebviewManager"; +import { WebviewManager, getWebviewContent } from "./WebviewManager"; import { initializeParser } from "./parser"; import { parseAndStore } from "./parser"; import { minimatch } from "minimatch"; @@ -219,9 +219,6 @@ async function workspaceScan(store: FileParseStore, webviewManager: WebviewManag webviewManager.broadcastFullState(fullState); } -// async function workspaceScan(): Promise { -// return await getJavaFiles(); -// } /** * Gets all java files within the workspace excluding the ones mentioned in .exclude. @@ -297,554 +294,6 @@ class CodescapeViewProvider implements vscode.WebviewViewProvider { } } -// new canvas-based city visualization that renders an isometric grid and buildings from AST data -export function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { - const rendererUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "src", "webview", "renderer.js"), - ); - const umlUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "src", "webview", "uml.js"), - ); - const layoutUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri,"src","webview","placer.js")); - - return ` - - - - - - - - - - - - - - `; -} // This method is called when your extension is deactivated export function deactivate() { } From 83c1fdd18c3efcd5b6a6afbfb21ff748def097e9 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Thu, 2 Apr 2026 15:52:22 -0400 Subject: [PATCH 11/15] merged main into renderer refactor for updated html --- extension/src/extension.ts | 48 ++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index ef35feb..bb6b1b4 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -2,12 +2,13 @@ // Import the module and reference it with the alias vscode in your code below import * as vscode from "vscode"; import * as path from "path"; -import { FileParseStore } from "./state"; -import { JavaFileWatcher } from "./JavaFileWatcher"; -import { WebviewManager, getWebviewContent } from "./WebviewManager"; -import { initializeParser } from "./parser"; -import { parseAndStore } from "./parser"; import { minimatch } from "minimatch"; +import { JavaFileWatcher } from "./JavaFileWatcher"; +import { WebviewManager } from "./WebviewManager"; +import { initializeParser, parseAndStore } from "./parser"; +import { buildCityWebviewHtml } from "./cityWebviewHtml"; +import { computeCityLayout } from "./cityLayout"; +import { FileParseStore } from "./state"; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -35,16 +36,16 @@ context.subscriptions.push( // multi panels const createSidePanel = vscode.commands.registerCommand('codescape.createSidePanel', () => { - webviewManager.createPanel('side'); + webviewManager.createWebview('side'); }); const createBottomPanel = vscode.commands.registerCommand('codescape.createBottomPanel', () => { - webviewManager.createPanel('bottom'); + webviewManager.createWebview('bottom'); }); // legacy command const create = vscode.commands.registerCommand('codescape.createPanel', () => { - webviewManager.createPanel('side'); + webviewManager.createWebview('side'); }); // Parse all existing Java and Python files on startup @@ -58,11 +59,12 @@ const create = vscode.commands.registerCommand('codescape.createPanel', () => { } // Send full state to webview manager after initial parse - const fullState = { - classes: store.snapshot().flatMap(e => e.entry.data ?? []), - status: 'ready' - }; - webviewManager.broadcastFullState(fullState); + const classes = store.snapshot().flatMap((entry) => entry.entry.data ?? []); + webviewManager.broadcastFullState({ + classes, + layout: computeCityLayout(classes), + status: classes.length > 0 ? "ready" : "empty", + }); const dumpDisposable = vscode.commands.registerCommand( "codescape.dumpParseStore", @@ -212,11 +214,12 @@ async function workspaceScan(store: FileParseStore, webviewManager: WebviewManag vscode.window.showInformationMessage(`Codescape: Scan complete! Successfully parsed ${successCount} files (${failureCount} failures).`); // Broadcast updated full state to all webviews - const fullState = { - classes: snap.flatMap(e => e.entry.data ?? []), - status: successCount > 0 ? 'ready' : 'empty' - }; - webviewManager.broadcastFullState(fullState); + const scannedClasses = snap.flatMap((entry) => entry.entry.data ?? []); + webviewManager.broadcastFullState({ + classes: scannedClasses, + layout: computeCityLayout(scannedClasses), + status: successCount > 0 && scannedClasses.length > 0 ? "ready" : "empty", + }); } @@ -276,6 +279,15 @@ export async function isExcluded(uri: vscode.Uri): Promise { .filter((line) => line.trim() !== ""); return excludeFiles.some((pattern) => minimatch(path, pattern)); } +function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string { + const rendererUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "src", "webview", "renderer.js") + ); + const umlUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "src", "webview", "uml.js") + ); + return buildCityWebviewHtml(rendererUri.toString(), umlUri.toString()); +} // sidebar view class CodescapeViewProvider implements vscode.WebviewViewProvider { From eb0724bf5e574001bdf74e96129e815e4cb4ee58 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Thu, 2 Apr 2026 20:09:07 -0400 Subject: [PATCH 12/15] refactored html so webviews html only handles event listeners --- extension/src/WebviewManager.ts | 8 +- extension/src/cityWebviewHtml.ts | 312 ++--------------------------- extension/src/extension.ts | 8 +- extension/src/webview/citystate.js | 298 +++++++++++++++++++++++++++ extension/src/webview/renderer.js | 4 +- 5 files changed, 321 insertions(+), 309 deletions(-) create mode 100644 extension/src/webview/citystate.js diff --git a/extension/src/WebviewManager.ts b/extension/src/WebviewManager.ts index dd6ba13..9cc2e8c 100644 --- a/extension/src/WebviewManager.ts +++ b/extension/src/WebviewManager.ts @@ -159,12 +159,12 @@ export class WebviewManager { } private getWebviewContent(webview: vscode.Webview): string { - const rendererUri = webview.asWebviewUri( - vscode.Uri.joinPath(this.extensionUri, 'src', 'webview', 'renderer.js') - ); const umlUri = webview.asWebviewUri( vscode.Uri.joinPath(this.extensionUri, 'src', 'webview', 'uml.js') ); - return buildCityWebviewHtml(rendererUri.toString(), umlUri.toString()); + const cityUri = webview.asWebviewUri( + vscode.Uri.joinPath(this.extensionUri, 'src','webview','citystate.js') + ) + return buildCityWebviewHtml(umlUri.toString(), cityUri.toString()); } } diff --git a/extension/src/cityWebviewHtml.ts b/extension/src/cityWebviewHtml.ts index fd21f0c..fad2ade 100644 --- a/extension/src/cityWebviewHtml.ts +++ b/extension/src/cityWebviewHtml.ts @@ -2,7 +2,7 @@ * Shared city canvas webview document (WebviewManager + explorer sidebar). * Keep in sync with extension ↔ webview message payloads (FULL_STATE / PARTIAL_STATE). */ -export function buildCityWebviewHtml(rendererUri: string, umlUri: string): string { +export function buildCityWebviewHtml(umlUri: string, cityUri : string): string { return ` @@ -14,334 +14,48 @@ export function buildCityWebviewHtml(rendererUri: string, umlUri: string): strin - - + diff --git a/extension/src/extension.ts b/extension/src/extension.ts index bb6b1b4..43759e8 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -280,13 +280,13 @@ export async function isExcluded(uri: vscode.Uri): Promise { return excludeFiles.some((pattern) => minimatch(path, pattern)); } function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string { - const rendererUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "src", "webview", "renderer.js") - ); const umlUri = webview.asWebviewUri( vscode.Uri.joinPath(extensionUri, "src", "webview", "uml.js") ); - return buildCityWebviewHtml(rendererUri.toString(), umlUri.toString()); + const cityUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "src", "webview", "citystate.js") + ) + return buildCityWebviewHtml(umlUri.toString(), cityUri.toString()); } // sidebar view diff --git a/extension/src/webview/citystate.js b/extension/src/webview/citystate.js new file mode 100644 index 0000000..6d3d77b --- /dev/null +++ b/extension/src/webview/citystate.js @@ -0,0 +1,298 @@ +import { placeIsoBuilding, drawIsoGrid } from './renderer.js' +const COLOR_PALETTE = [ + "#598BAF", + "#8B5CF6", + "#10B981", + "#F59E0B", + "#EF4444", + "#14B8A6", + "#6366F1", + "#EC4899" +]; +const TILE_L = 50; + +export class CityState { + + constructor(canvas, ctx) { + this.canvas = canvas; + this.ctx = ctx; + this.state = { + classes: [], + layout: {}, + colors: {}, + status: "loading", + classMap: {} + }; + this.buildingRegistry = [] + this.offsetX = canvas.width / 2; + this.offsetY = 100; + this.zoomLevel = 1; + + } + effectiveFloors(cls) { + const methods = cls.Methods || []; + const fields = cls.Fields || []; + let m = methods.length; + let f = fields.length; + if (cls.Type === 'module') { + const codeBlocks = methods.filter((x) => { + return x.name && String(x.name).indexOf(' { return !c.parentClass; }); + const inner = this.state.classes.filter((c) => { return c.parentClass; }); + topLevel.forEach((cls, index) => { + this.state.layout[cls.Classname] = { col: 3 + index * 2, row: 3 + index, depth: 0 }; + }); + inner.forEach((cls) => { + const parent = this.state.classMap[cls.parentClass]; + const pp = parent && this.state.layout[parent.Classname]; + if (pp) { + this.state.layout[cls.Classname] = { + col: pp.col + 2, + row: pp.row + 1, + depth: (pp.depth || 0) + 1 + }; + } else { + this.state.layout[cls.Classname] = { col: 20, row: 10, depth: 1 }; + } + }); + } + + rebuildClassMap() { + this.state.classMap = {}; + this.state.classes.forEach((cls) => { + this.state.classMap[cls.Classname] = cls; + }); + } + + applyFullPayload(payload) { + const classes = payload.classes || []; + this.state.classes = classes; + this.rebuildClassMap(); + + const layout = payload.layout; + if (layout && typeof layout === 'object' && Object.keys(layout).length > 0) { + this.state.layout = layout; + } else { + this.runAutoLayoutFallback(); + } + + if (!classes.length) { + this.state.status = 'empty'; + } else { + this.state.status = 'ready'; + } + + this.offsetX = this.canvas.width / 2; + this.offsetY = 100; + this.assignColors(); + this.render(); + } + + applyPartialPayload(payload) { + if (payload.fullClasses && payload.layout) { + this.applyFullPayload({ + classes: payload.fullClasses, + layout: payload.layout, + status: 'ready' + }); + } else { + const changed = payload.changed || []; + const related = payload.related || []; + const removed = payload.removed || []; + const map = {}; + + this.state.classes.forEach((c) => { map[c.Classname] = c; }); + removed.forEach((name) => { delete map[name]; }); + const upsert = changed.length ? changed : related; + upsert.forEach((c) => { map[c.Classname] = c; }); + this.state.classes = Object.keys(map).map((k) => { return map[k]; }); + this.rebuildClassMap(); + this.runAutoLayoutFallback(); + this.assignColors(); + this.render(); + } + } + + assignColors() { + const newColorMap = {}; + const usedColors = new Set(); + + this.state.classes.forEach((cls) => { + const existing = this.state.colors[cls.Classname]; + if (existing) { + newColorMap[cls.Classname] = existing; + usedColors.add(existing); + } + }); + + this.state.classes.forEach((cls) => { + if (!newColorMap[cls.Classname]) { + let candidate = this.pickBaseColor(cls); + let tries = 0; + while (usedColors.has(candidate) && tries < 24) { + candidate = COLOR_PALETTE[(this.hashHue(cls.Classname + tries)) % COLOR_PALETTE.length]; + tries++; + } + newColorMap[cls.Classname] = candidate; + usedColors.add(candidate); + } + }); + + this.state.colors = newColorMap; + } + + render() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.save(); + this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); + this.ctx.scale(this.zoomLevel, this.zoomLevel); + this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); + + this.offsetX = this.canvas.width / 2; + this.offsetY = 100; + drawIsoGrid(this.ctx, 10, 10, TILE_L, this.offsetX, this.offsetY); + + if (this.state.status === "loading") { + this.drawLoadingMessage(); + this.ctx.restore(); + return; + } + + if (this.state.status === "empty") { + this.drawEmptyMessage(); + this.ctx.restore(); + return; + } + + if (this.state.status === "error") { + this.drawErrorMessage(); + this.ctx.restore(); + return; + } + + this.buildingRegistry.length = 0; + this.state.classes.forEach((cls) => { + const position = this.state.layout[cls.Classname]; + if (!position) return; + + const floors = this.effectiveFloors(cls); + const depthScale = 1 - ((position.depth || 0) * 0.12); + const adjustedFloors = Math.max(1, Math.ceil(floors * Math.max(0.5, depthScale))); + + const col = position.col; + const row = position.row; + const isoX = (col - row) * TILE_L / 2 + this.offsetX; + const isoY = (col + row) * TILE_L / 4 + this.offsetY + TILE_L / 2; + const approxHeight = TILE_L + adjustedFloors * (TILE_L / 2); + this.buildingRegistry.push({ + className: cls.Classname, + x: isoX - TILE_L / 2, + y: isoY - approxHeight, + width: TILE_L, + height: approxHeight + }); + + placeIsoBuilding( + this.ctx, + position.col, + position.row, + adjustedFloors, + this.state.colors[cls.Classname] || this.pickBaseColor(cls), + TILE_L, + this.offsetX, + this.offsetY + ); + + if (cls.parentClass) { + const parentPos = this.state.layout[cls.parentClass]; + if (parentPos) { + const fromWorld = this.colRowToWorld(parentPos.col, parentPos.row, TILE_L, this.offsetX, this.offsetY); + const toWorld = this.colRowToWorld(position.col, position.row, TILE_L, this.offsetX, this.offsetY); + this.ctx.save(); + this.ctx.strokeStyle = "rgba(200, 200, 200, 0.5)"; + this.ctx.lineWidth = 1; + this.ctx.setLineDash([2, 2]); + this.ctx.beginPath(); + this.ctx.moveTo(fromWorld.x, fromWorld.y); + this.ctx.lineTo(toWorld.x, toWorld.y); + this.ctx.stroke(); + this.ctx.restore(); + } + } + }); + + this.ctx.restore(); + } + + colRowToWorld(col, row, tileL, ox, oy) { + const x = ox + (col - row) * (tileL / 2); + const y = oy + (col + row) * (tileL / 4); + return { x: x, y: y }; + } + + drawLoadingMessage() { + this.ctx.fillStyle = "white"; + this.ctx.font = "20px Arial"; + this.ctx.fillText("Loading...", 50, 50); + } + + drawEmptyMessage() { + this.ctx.fillStyle = "white"; + this.ctx.font = "20px Arial"; + this.ctx.fillText("No classes detected.", 50, 50); + } + + drawErrorMessage() { + this.ctx.fillStyle = "red"; + this.ctx.font = "20px Arial"; + this.ctx.fillText("Error parsing files.", 50, 50); + } + screenToWorld(clientX, clientY) { + const x = (clientX - this.canvas.width / 2) / this.zoomLevel + this.canvas.width / 2; + const y = (clientY - this.canvas.height / 2) / this.zoomLevel + this.canvas.height / 2; + return { x: x, y: y }; + } + + getBuildingAtPosition(canvasX, canvasY) { + for (let i = this.buildingRegistry.length - 1; i >= 0; i--) { + const b = this.buildingRegistry[i]; + const inside = + canvasX >= b.x && + canvasX <= b.x + b.width && + canvasY >= b.y && + canvasY <= b.y + b.height; + if (inside) return b; + } + return null; + } +} \ No newline at end of file diff --git a/extension/src/webview/renderer.js b/extension/src/webview/renderer.js index 2efb374..4048081 100644 --- a/extension/src/webview/renderer.js +++ b/extension/src/webview/renderer.js @@ -1,5 +1,5 @@ // Isometric grid rendering -function drawIsoGrid(ctx, rows, cols, size, offsetX, offsetY) { +export function drawIsoGrid(ctx, rows, cols, size, offsetX, offsetY) { ctx.strokeStyle = '#2c2c2c'; var tileW = size; var tileH = size / 2; @@ -92,7 +92,7 @@ function drawIsoBuilding(ctx, baseX, baseY, floors, size, color) { } // Place building on grid -function placeIsoBuilding(ctx, col, row, floors, color, TILE_L, offsetX, offsetY) { +export function placeIsoBuilding(ctx, col, row, floors, color, TILE_L, offsetX, offsetY) { var isoX = (col - row) * TILE_L / 2 + offsetX; var isoY = (col + row) * TILE_L / 4 + offsetY; drawIsoBuilding(ctx, isoX, isoY + TILE_L / 2, floors, TILE_L, color || '#598BAF'); From 23fbf4ec27ebc7a8fc7d7c2e800b996165436a2c Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Thu, 2 Apr 2026 20:27:02 -0400 Subject: [PATCH 13/15] Update extension/src/cityWebviewHtml.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extension/src/cityWebviewHtml.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extension/src/cityWebviewHtml.ts b/extension/src/cityWebviewHtml.ts index fad2ade..fb29c09 100644 --- a/extension/src/cityWebviewHtml.ts +++ b/extension/src/cityWebviewHtml.ts @@ -33,8 +33,7 @@ export function buildCityWebviewHtml(umlUri: string, cityUri : string): string { if (msg.type === 'FULL_STATE' && msg.payload && msg.payload.classes) { city.applyFullPayload(msg.payload); } else if (msg.type === 'AST_DATA' && msg.payload && msg.payload.files) { - state.status = 'empty'; - city.render(); + city.applyFullPayload({ classes: [] }); } else if (msg.type === 'PARTIAL_STATE' && msg.payload) { city.applyPartialPayload(msg.payload); } From 4fe06cffac65d2c25b9517c51d55f6c4245f6a23 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Thu, 2 Apr 2026 20:34:43 -0400 Subject: [PATCH 14/15] Update .github/workflows/ci.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7002bae..7795587 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,12 +3,14 @@ name: CI on: push: branches: [main] - paths: + paths: - 'media/**' + - 'extension/**' pull_request: branches: [main] paths: - 'media/**' + - 'extension/**' jobs: extension: From 6d1ebc806f947d8c3478436d73cf83efdd9f08d3 Mon Sep 17 00:00:00 2001 From: Logan Tom Date: Thu, 2 Apr 2026 20:35:38 -0400 Subject: [PATCH 15/15] removed placer.js because its purpose is redundant --- extension/src/cityWebviewHtml.ts | 4 +-- extension/src/webview/placer.js | 44 -------------------------------- 2 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 extension/src/webview/placer.js diff --git a/extension/src/cityWebviewHtml.ts b/extension/src/cityWebviewHtml.ts index fad2ade..363c249 100644 --- a/extension/src/cityWebviewHtml.ts +++ b/extension/src/cityWebviewHtml.ts @@ -58,8 +58,8 @@ export function buildCityWebviewHtml(umlUri: string, cityUri : string): string { canvas.addEventListener('click', function (e) { - const world = screenToWorld(e.clientX, e.clientY); - const building = getBuildingAtPosition(world.x, world.y); + const world = city.screenToWorld(canvasX, canvasY); + const building = city.getBuildingAtPosition(world.x, world.y); if (!building) return; vscode.postMessage({ type: 'OPEN_CLASS_SOURCE', diff --git a/extension/src/webview/placer.js b/extension/src/webview/placer.js deleted file mode 100644 index 26b4a08..0000000 --- a/extension/src/webview/placer.js +++ /dev/null @@ -1,44 +0,0 @@ -function computeLayout(nodes) { - const layout = {}; - let row = 0; - const placed = new Set(); - - const topLevel = nodes.filter(n => !n.parentClass); - const innerClasses = nodes.filter(n => n.parentClass); - - for (const node of topLevel) { - if (!placed.has(node.id)) { - layout[node.id] = { col: 0, row, depth: 0 }; - placed.add(node.id); - let col = 1; - for (const neighbor of node.neighbors) { - if (!placed.has(neighbor) && !innerClasses.some(ic => ic.id === neighbor)) { - layout[neighbor] = { col, row, depth: 0 }; - placed.add(neighbor); - col++; - } - } - row++; - } - } - - for (const innerClass of innerClasses) { - if (!placed.has(innerClass.id)) { - const parentLayout = layout[innerClass.parentClass]; - if (parentLayout) { - layout[innerClass.id] = { - col: parentLayout.col + 1, - row: parentLayout.row, - depth: (parentLayout.depth || 0) + 1 - }; - placed.add(innerClass.id); - } else { - layout[innerClass.id] = { col: 0, row, depth: 1 }; - placed.add(innerClass.id); - row++; - } - } - } - - return layout; -}