Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Write Your Python Program - CHANGELOG

* 2.1.1 (2025-11-26)
* Fix a couple of glitches with highlighting in the visualization
* 2.1.0 (2025-11-07)
* Fix bug with recursive types #192
* Improve startup performance and other minor improvements #190
Expand Down
20 changes: 10 additions & 10 deletions media/programflow-visualization/webview.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ window.addEventListener("message", (event) => {
/**
* Updates the Visualization in the Webview, with the given BackendTraceElem.
*
* @param traceElem A BackendTraceElem with 3 fields (line, stack, heap)
* @param traceElem A FrontendTraceElem
*/
function updateVisualization(traceElem) {
const data = `
Expand All @@ -56,10 +56,10 @@ function updateVisualization(traceElem) {
</div>
<div class="row">
<div class="column floating-left floating-left-content" id="frames">
${traceElem[1]}
${traceElem.stackHTML}
</div>
<div class="column floating-right floating-right-content" id="objects">
${traceElem[2]}
${traceElem.heapHTML}
</div>
</div>
`;
Expand All @@ -78,17 +78,17 @@ function updateVisualization(traceElem) {
}

const stdoutLog = document.getElementById("stdout-log");
stdoutLog.innerHTML = traceElem[4];
stdoutLog.innerHTML = traceElem.outputState;
stdoutLog.scrollTo(0, stdoutLog.scrollHeight);
}

/**
* Updates the indendation for heap elements, if a other heap element references it.
*
* @param traceElem A BackendTraceElem with 3 fields (line, stack, heap)
* @param traceElem A FrontendTraceElem
*/
function updateIntend(traceElem) {
const heapTags = traceElem[2].match(/(?<=startPointer)[0-9]+/g);
const heapTags = traceElem.heapHTML.match(/(?<=startPointer)[0-9]+/g);
if (heapTags) {
heapTags.forEach((tag) => {
const element = document.getElementById("objectItem" + tag);
Expand Down Expand Up @@ -117,7 +117,7 @@ function updateRefArrows(traceElem) {
return new LinkerLine({
parent: document.getElementById("viz"),
start: tag.elem1,
end: tag.elem2,
end: tag.elem2,
size: 2,
path: "magnet",
startSocket: "right",
Expand All @@ -139,9 +139,9 @@ function updateRefArrows(traceElem) {
* @returns A list with all ids that have either a start or end pointer id in the html
*/
function getCurrentTags(traceElem) {
const stackTags = traceElem[1].match(/(?<=id=")(.+)Pointer[0-9]+/g);
const heapTags = traceElem[2].match(/(?<=startPointer)[0-9]+/g);
const uniqueId = traceElem[2].match(/(?<=)\d+(?=startPointer)/g);
const stackTags = traceElem.stackHTML.match(/(?<=id=")(.+)Pointer[0-9]+/g);
const heapTags = traceElem.heapHTML.match(/(?<=startPointer)[0-9]+/g);
const uniqueId = traceElem.heapHTML.match(/(?<=)\d+(?=startPointer)/g);

if (!stackTags) {
return;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Write Your Python Program!",
"description": "A user friendly python environment for beginners",
"license": "See license in LICENSE",
"version": "2.1.0",
"version": "2.1.1",
"publisher": "StefanWehr",
"icon": "icon.png",
"engines": {
Expand Down
8 changes: 7 additions & 1 deletion src/programflow-visualization/frontend/HTMLGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ export class HTMLGenerator {
if (traceElement.traceback !== undefined) {
output += `<span class="traceback-text">${traceElement.traceback}</span>`;
}
return [traceElement.line, frameItems, objectItems, traceElement.filePath, output];
return {
lineNumber: traceElement.line,
stackHTML: frameItems,
heapHTML: objectItems,
filename: traceElement.filePath,
outputState: output
};
}

private objectItem(name: string, value: HeapValue): string {
Expand Down
4 changes: 3 additions & 1 deletion src/programflow-visualization/frontend/frontend.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExtensionContext } from 'vscode';
import * as vscode from 'vscode';
import { VisualizationPanel } from './visualization_panel';
import { MessagePort } from 'worker_threads';
import * as TraceCache from '../trace_cache';
Expand All @@ -7,6 +8,7 @@ let panel: VisualizationPanel | undefined = undefined;

export async function startFrontend(
context: ExtensionContext,
outChannel: vscode.OutputChannel,
filePath: string,
fileHash: string,
tracePort: MessagePort | null): Promise<Failure | undefined> {
Expand All @@ -16,7 +18,7 @@ export async function startFrontend(
}

panel?.dispose();
panel = await VisualizationPanel.getVisualizationPanel(context, filePath, fileHash, trace, tracePort);
panel = await VisualizationPanel.getVisualizationPanel(context, outChannel, filePath, fileHash, trace, tracePort);
if (!panel) {
return failure("Frontend couldn't be initialized!");
}
Expand Down
94 changes: 64 additions & 30 deletions src/programflow-visualization/frontend/visualization_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ export class VisualizationPanel {
private _trace: FrontendTrace;
private _traceIndex: number;
private _tracePortSelfClose: boolean;
private _outChannel: vscode.OutputChannel;

private constructor(context: vscode.ExtensionContext, filePath: string, fileHash: string, trace: BackendTrace, tracePort: MessagePort | null) {
private constructor(
context: vscode.ExtensionContext,
outChannel: vscode.OutputChannel,
filePath: string,
fileHash: string,
trace: BackendTrace,
tracePort: MessagePort | null
) {
this._outChannel = outChannel;
this._fileHash = fileHash;
this._tracePort = tracePort;
this._backendTrace = { trace: trace, complete: trace.length > 0 };
Expand Down Expand Up @@ -74,8 +83,8 @@ export class VisualizationPanel {
if (this._panel?.active) {
this.updateLineHighlight();
}
}, undefined, context.subscriptions);
}, undefined, context.subscriptions);


// Message Receivers
this._panel.webview.onDidReceiveMessage(
Expand All @@ -100,7 +109,7 @@ export class VisualizationPanel {
this._trace.push((new HTMLGenerator()).generateHTML(backendTraceElem));
await this.postMessagesToWebview('updateButtons', 'updateContent');
if (firstElement) {
this.updateLineHighlight();
await this.updateLineHighlight();
}
});
this._tracePort?.on('close', async () => {
Expand All @@ -118,12 +127,13 @@ export class VisualizationPanel {

public static async getVisualizationPanel(
context: vscode.ExtensionContext,
outChannel: vscode.OutputChannel,
filePath: string,
fileHash: string,
trace: BackendTrace,
tracePort: MessagePort | null
): Promise<VisualizationPanel | undefined> {
return new VisualizationPanel(context, filePath, fileHash, trace, tracePort);
return new VisualizationPanel(context, outChannel, filePath, fileHash, trace, tracePort);
}

// TODO: Look if Typescript is possible OR do better documentation in all files
Expand Down Expand Up @@ -185,35 +195,59 @@ export class VisualizationPanel {
}

private async updateLineHighlight(remove: boolean = false) {
if (this._trace.length === 0) {
return;
}
let editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.filter(
editor => path.basename(editor.document.uri.path) === path.basename(this._trace[this._traceIndex][3]!)
)[0];
try {
if (this._trace.length === 0) {
this._outChannel.appendLine("updateLineHighlight: no trace available, aborting");
return;
}
const traceFile = this._trace[this._traceIndex].filename;
this._outChannel.appendLine(
`updateLineHighlight: traceFile=${traceFile}, traceIndex=${this._traceIndex}, remove=${remove}`);

const openPath = vscode.Uri.parse(this._trace[this._traceIndex][3]!);
if (!editor || editor.document.uri.path !== openPath.path) {
await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup');
const document = await vscode.workspace.openTextDocument(openPath);
editor = await vscode.window.showTextDocument(document);
}
// Use vscode.Uri.file() for proper file path to URI conversion
const openPath = vscode.Uri.file(traceFile);

if (remove || editor.document.lineCount < this._trace[this._traceIndex][0]) {
editor.setDecorations(nextLineExecuteHighlightType, []);
} else {
this.setNextLineHighlighting(editor);
}
}
// Find editor by full normalized path, not just basename
let editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find(
editor => editor.document.uri.fsPath === openPath.fsPath
);

private setNextLineHighlighting(editor: vscode.TextEditor) {
if (this._trace.length === 0) {
return;
}
const nextLine = this._trace[this._traceIndex][0] - 1;
if (!editor && remove) {
return;
} else if (!editor){
this._outChannel.appendLine(`updateLineHighlight: editor not found, opening document: ${openPath.fsPath}`);
await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup');
const document = await vscode.workspace.openTextDocument(openPath);
editor = await vscode.window.showTextDocument(document, { preserveFocus: false });
// Give the editor time to fully initialize
await new Promise(resolve => setTimeout(resolve, 100));
if (!editor) {
this._outChannel.appendLine(`updateLineHighlight: failed to get editor after opening document`);
return;
}
}

if (nextLine > -1) {
this.setEditorDecorations(editor, nextLineExecuteHighlightType, nextLine);
const traceLine = this._trace[this._traceIndex].lineNumber;
const lineNo = traceLine - 1; // zero-based indexing in vscode
if (remove) {
this._outChannel.appendLine(
"updateLineHighlight: removing highlighting in " + editor.document.fileName);
editor.setDecorations(nextLineExecuteHighlightType, []);
} else if (lineNo < 0 || lineNo >= editor.document.lineCount) {
this._outChannel.appendLine(
"updateLineHighlight: traceLine " + traceLine + " out of range (doc has " +
editor.document.lineCount + " lines) in " + editor.document.fileName);
editor.setDecorations(nextLineExecuteHighlightType, []);
} else {
this._outChannel.appendLine(
"updateLineHighlight: highlighting line " + traceLine + " in " + editor.document.fileName);
this.setEditorDecorations(editor, nextLineExecuteHighlightType, lineNo);
}
} catch (error) {
this._outChannel.appendLine(`updateLineHighlight: ERROR - ${error}`);
if (error instanceof Error) {
this._outChannel.appendLine(`Stack: ${error.stack}`);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/programflow-visualization/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function getProgFlowVizCallback(context: vscode.ExtensionContext, outChan
tracePort = startBackend(context, file, outChannel);
}

const result = await startFrontend(context, file.fsPath, fileHash, tracePort);
const result = await startFrontend(context, outChannel, file.fsPath, fileHash, tracePort);
if (result) {
await vscode.window.showErrorMessage("Error ProgramFlow-Visualization: " + result.errorMessage);
return;
Expand Down
12 changes: 10 additions & 2 deletions src/programflow-visualization/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/**
* For better readable code
*/
type Try = Success | Failure;
Expand All @@ -7,7 +7,15 @@ type Failure = { errorMessage: string };

// State Types for the Frontend
type FrontendTrace = Array<FrontendTraceElem>;
type FrontendTraceElem = [number, string, string, string, string];

type FrontendTraceElem = {
lineNumber: number, // 1-based
stackHTML: string,
heapHTML: string,
filename: string,
outputState: string,
};

// ############################################################################################
// State Types for the Backend
type PartialBackendTrace = {
Expand Down
3 changes: 2 additions & 1 deletion vscode-test/type-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class Point:
x: int
y: int

p = Point()
p = Point(1, 2)
print(p)