Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
534de31
Init new ManimShell wrapper
Splines Oct 25, 2024
7c86072
Include ManimShell as additional layer in other commands
Splines Oct 25, 2024
7436cb0
Wait until command in IPython Terminal is finished
Splines Oct 25, 2024
34046ab
Detect shell exit
Splines Oct 25, 2024
6411241
Open terminal upon error
Splines Oct 25, 2024
5511f5d
Improve docstrings & implement executeCommandEnsureActiveSession
Splines Oct 25, 2024
1a06808
Add docstrings for `exec` method
Splines Oct 25, 2024
1ed9546
Add docstrings to `retrieveOrInitActiveShell`
Splines Oct 25, 2024
70aabd5
Add docstrings for `initiateTerminalDataReading`
Splines Oct 25, 2024
ab096bd
Add docstrings for ANSI filtering
Splines Oct 25, 2024
8f8d254
Add docstrings to regular expressions
Splines Oct 25, 2024
d426004
Remove log line
Splines Oct 25, 2024
576d1a4
Fix wrong variable usage
Splines Oct 25, 2024
d1f30d8
Simplify overly complicated Promises/callbacks
Splines Oct 25, 2024
50181e9
Explain only divergence of notation
Splines Oct 25, 2024
75c519f
Don't carry optional parameter startLine around
Splines Oct 25, 2024
7c083c4
Simplify execute command waiting
Splines Oct 25, 2024
d75681a
Move resetActiveShell method down
Splines Oct 25, 2024
6c15e6e
Extract method hasActiveShell()
Splines Oct 25, 2024
e5d1576
Exit scene if start scene is invoked while scene is running
Splines Oct 25, 2024
6edb693
Make method private
Splines Oct 25, 2024
947dc7a
Import Terminal directly
Splines Oct 25, 2024
93f7560
Clarify what ManimShell is
Splines Oct 25, 2024
cc81d62
Add docstring for detectShellExecutionEnd
Splines Oct 25, 2024
5321eaa
Fix typos in docstrings
Splines Oct 25, 2024
d035d69
Lock new command execution during startup
Splines Oct 25, 2024
a5f86e9
Don't be stingy with empty lines
Splines Oct 25, 2024
835e8b6
Avoid orphan VSCode terminals upon scene start
Splines Oct 25, 2024
896cc5e
Merge branch 'main' into feature/manim-shell
Splines Oct 25, 2024
f6045be
Remove unnecessary statements
Splines Oct 25, 2024
bd40282
Listen to terminal data right from the beginning
Splines Oct 25, 2024
4d41027
Fix clipboard buffering
Splines Oct 25, 2024
6cad6ec
Prevent multiple commands from running on MacOS
Splines Oct 26, 2024
10118f0
Add progress notification to scene startup
Splines Oct 26, 2024
072fa74
Remove console log
Splines Oct 26, 2024
fb59410
Merge branch 'fix/overlapping-commands' into feature/progress-notific…
Splines Oct 26, 2024
ef553fe
Init preview progress notification
Splines Oct 26, 2024
3220c55
Delete console logs
Splines Oct 26, 2024
23fba1f
Introduce keyboard interrupt event
Splines Oct 26, 2024
91dd6f8
Restore checkpoint paste command
Splines Oct 26, 2024
4bd15b6
Extract method waitUntilCommandFinished & unify behavior
Splines Oct 26, 2024
b52f0e4
Remove fulfilled TODO note
Splines Oct 26, 2024
e896440
Init PreviewProgress class with more sophisticated handling
Splines Oct 26, 2024
715e9fd
Update MacOS error message
bhoov Oct 27, 2024
65ed0c1
(fix MacOS typo)
bhoov Oct 27, 2024
cc725ba
Merge branch 'main' into fix/overlapping-commands
Splines Oct 27, 2024
6cec111
Wrap callback in interface (for future PR)
Splines Oct 27, 2024
1ca9fbc
Refactor execute command methods (merge into one)
Splines Oct 27, 2024
d20d947
Throw error if no active session found (but required)
Splines Oct 27, 2024
0116318
Remove unnecessary constructor
Splines Oct 27, 2024
6cc3172
Merge branch 'fix/overlapping-commands' into feature/progress-notific…
Splines Oct 27, 2024
7cd8bed
Add docstring to DATA event
Splines Oct 27, 2024
fbaaf00
Remove leftover variables
Splines Oct 27, 2024
3b7f331
Finish progress even if error
Splines Oct 27, 2024
db998a5
Merge branch 'main' into feature/progress-notification
Splines Oct 27, 2024
a58537d
Keep track of IPython cell number
Splines Oct 27, 2024
f513334
Use await for previewCode
Splines Oct 27, 2024
14bc6ae
Use command waiting function in start command execution
Splines Oct 27, 2024
701e9a9
Exit scene if Manim detected in new terminal
Splines Oct 27, 2024
f25e8f4
Add docstrings to waitUntilCommandFinished
Splines Oct 27, 2024
ddf1c19
Add docstring to iPythonCellCount
Splines Oct 27, 2024
e47d259
Add docstrings to previewCode
Splines Oct 27, 2024
3b81734
Remove TODO note (is explained in PR comment)
Splines Oct 27, 2024
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
8 changes: 4 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function deactivate() { }
* (when accessed via the command pallette) or the code of the cell where
* the codelens was clicked.
*/
function previewManimCell(cellCode?: string, startLine?: number) {
async function previewManimCell(cellCode?: string, startLine?: number) {
let startLineFinal: number | undefined = startLine;

// User has executed the command via command pallette
Expand Down Expand Up @@ -86,13 +86,13 @@ function previewManimCell(cellCode?: string, startLine?: number) {
return;
}

previewCode(cellCode, startLineFinal);
await previewCode(cellCode, startLineFinal);
}

/**
* Previews the Manim code of the selected text.
*/
function previewSelection() {
async function previewSelection() {
const editor = window.activeTextEditor;
if (!editor) {
window.showErrorMessage('Select some code to preview.');
Expand All @@ -119,7 +119,7 @@ function previewSelection() {
return;
}

previewCode(selectedText, selection.start.line);
await previewCode(selectedText, selection.start.line);
}

/**
Expand Down
139 changes: 120 additions & 19 deletions src/manimShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const ANSI_CONTROL_SEQUENCE_REGEX = /(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x
*/
const IPYTHON_CELL_START_REGEX = /^\s*In \[\d+\]:/m;

/**
* Regular expression to match a KeyboardInterrupt.
*/
const KEYBOARD_INTERRUPT_REGEX = /^\s*KeyboardInterrupt/m;

/**
* Regular expression to match an error message in the terminal. For any error
* message, IPython prints "Cell In[<number>], line <number>" to a new line.
Expand All @@ -35,6 +40,18 @@ enum ManimShellEvent {
* IPYTHON_CELL_START_REGEX is matched.
*/
IPYTHON_CELL_FINISHED = 'ipythonCellFinished',

/**
* Event emitted when a keyboard interrupt is detected in the terminal, e.g.
* when `Ctrl+C` is pressed to stop the current command execution.
*/
KEYBOARD_INTERRUPT = 'keyboardInterrupt',

/**
* Event emitted when data is received from the terminal, but stripped of
* ANSI control codes.
*/
DATA = 'ansiStrippedData'
}

/**
Expand All @@ -47,6 +64,15 @@ export interface CommandExecutionEventHandler {
* executing.
*/
onCommandIssued?: () => void;

/**
* Callback that is invoked when data is received from the active Manim
* session (IPython shell).
*
* @param data The data that was received from the terminal, but stripped
* of ANSI control codes.
*/
onData?: (data: string) => void;
}

/**
Expand All @@ -72,6 +98,13 @@ export class ManimShell {
private activeShell: Terminal | null = null;
private eventEmitter = new EventEmitter();

/**
* The current IPython cell count. Updated whenever a cell indicator is
* detected in the terminal output. This is used to determine when a new cell
* has started, i.e. when the command has finished executing.
*/
private iPythonCellCount: number = 0;

/**
* Whether the execution of a new command is locked. This is used to prevent
* multiple new scenes from being started at the same time, e.g. when users
Expand Down Expand Up @@ -202,12 +235,18 @@ export class ManimShell {
throw new NoActiveShellError();
}

if (this.shouldLockDuringCommandExecution && !forceExecute && this.isExecutingCommand) {
window.showWarningMessage(
`Simultaneous Manim commands are not currently supported on MacOS. `
+ `Please wait for the current operations to finish before initiating `
+ `a new command.`);
return;
if (this.isExecutingCommand) {
// MacOS specific behavior
if (this.shouldLockDuringCommandExecution && !forceExecute) {
window.showWarningMessage(
`Simultaneous Manim commands are not currently supported on MacOS. `
+ `Please wait for the current operations to finish before initiating `
+ `a new command.`);
return;
}

this.sendKeyboardInterrupt();
await new Promise(resolve => setTimeout(resolve, 500));
}

this.isExecutingCommand = true;
Expand All @@ -221,18 +260,22 @@ export class ManimShell {
this.lockDuringStartup = false;
}

const dataListener = (data: string) => { handler?.onData?.(data); };
this.eventEmitter.on(ManimShellEvent.DATA, dataListener);

let currentExecutionCount = this.iPythonCellCount;
console.log(`💨: ${currentExecutionCount}`);

this.exec(shell, command);
handler?.onCommandIssued?.();

if (waitUntilFinished) {
await new Promise(resolve => {
this.eventEmitter.once(ManimShellEvent.IPYTHON_CELL_FINISHED, resolve);
});
}

this.eventEmitter.once(ManimShellEvent.IPYTHON_CELL_FINISHED, () => {
this.waitUntilCommandFinished(currentExecutionCount, () => {
this.isExecutingCommand = false;
this.eventEmitter.off(ManimShellEvent.DATA, dataListener);
});
if (waitUntilFinished) {
await this.waitUntilCommandFinished(currentExecutionCount);
}
}

/**
Expand Down Expand Up @@ -266,11 +309,16 @@ export class ManimShell {
}
this.activeShell = window.createTerminal();
}
// We are sure that the active shell is set since it is invoked
// in `retrieveOrInitActiveShell()` or in the line above.
this.exec(this.activeShell as Terminal, command);
await new Promise(resolve => {
this.eventEmitter.once(ManimShellEvent.IPYTHON_CELL_FINISHED, resolve);

await window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Starting Manim...",
cancellable: false
}, async (progress, token) => {
// We are sure that the active shell is set since it is invoked
// in `retrieveOrInitActiveShell()` or in the line above.
this.exec(this.activeShell as Terminal, command);
await this.waitUntilCommandFinished(this.iPythonCellCount);
});
}

Expand All @@ -279,7 +327,9 @@ export class ManimShell {
* command execution.
*/
public resetActiveShell() {
this.iPythonCellCount = 0;
this.activeShell = null;
this.eventEmitter.removeAllListeners();
}

/**
Expand Down Expand Up @@ -336,6 +386,7 @@ export class ManimShell {
* @param command The command to execute in the shell.
*/
private exec(shell: Terminal, command: string) {
console.log(`🌟: ${command}`);
this.detectShellExecutionEnd = false;
if (shell.shellIntegration) {
shell.shellIntegration.executeCommand(command);
Expand All @@ -362,6 +413,42 @@ export class ManimShell {
return this.activeShell as Terminal;
}

/**
* Waits until the current command has finished executing by waiting for
* the start of the next IPython cell. This is used to ensure that the
* command has actually finished executing, e.g. when the whole animation
* has been previewed.
*
* @param currentExecutionCount The current IPython cell count when the
* command was issued. This is used to detect when the next cell has started.
* @param callback An optional callback that is invoked when the command
* has finished executing. This is useful when the caller does not want to
* await the async function, but still wants to execute some code after the
* command has finished.
*/
private async waitUntilCommandFinished(
currentExecutionCount: number, callback?: () => void) {
await new Promise<void>(resolve => {
this.eventEmitter.once(ManimShellEvent.KEYBOARD_INTERRUPT, resolve);

const listener = () => {
console.log(`🎵: ${this.iPythonCellCount} vs. ${currentExecutionCount}`);
if (this.iPythonCellCount > currentExecutionCount) {
this.eventEmitter.off(ManimShellEvent.IPYTHON_CELL_FINISHED, listener);
resolve();
}
};
this.eventEmitter.on(ManimShellEvent.IPYTHON_CELL_FINISHED, listener);
});
if (callback) {
callback();
}
}

private async sendKeyboardInterrupt() {
this.activeShell?.sendText('\x03'); // send `Ctrl+C`
}

/**
* Inits the reading of data from the terminal and issues actions/events
* based on the data received:
Expand All @@ -379,12 +466,26 @@ export class ManimShell {
async (event: vscode.TerminalShellExecutionStartEvent) => {
const stream = event.execution.read();
for await (const data of withoutAnsiCodes(stream)) {
console.log(`🎯: ${data}`);

this.eventEmitter.emit(ManimShellEvent.DATA, data);

if (data.match(MANIM_WELCOME_REGEX)) {
if (this.activeShell && this.activeShell !== event.terminal) {
exitScene(); // Manim detected in new terminal
}
this.activeShell = event.terminal;
}

if (data.match(IPYTHON_CELL_START_REGEX)) {
if (data.match(KEYBOARD_INTERRUPT_REGEX)) {
this.eventEmitter.emit(ManimShellEvent.KEYBOARD_INTERRUPT);
}

let ipythonMatch = data.match(IPYTHON_CELL_START_REGEX);
if (ipythonMatch) {
const cellNumber = parseInt(ipythonMatch[0].match(/\d+/)![0]);
this.iPythonCellCount = cellNumber;
console.log(`🎧: ${cellNumber}`);
this.eventEmitter.emit(ManimShellEvent.IPYTHON_CELL_FINISHED);
}

Expand Down
Loading