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
71 changes: 71 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: CI

on:
push:
branches: [main]
tags: ["v*.*.*"]
pull_request:
branches: [main]

jobs:
build:
name: Build & Package
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Compile TypeScript
run: npx tsc -p . --noEmit

- name: Package extension
run: npm run package

- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: sciml-toolkit-vsix
path: "*.vsix"
retention-days: 30

release:
name: Create Release
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')

permissions:
contents: write

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install & package
run: npm ci && npm run package

- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: "SciML Toolkit ${{ steps.version.outputs.VERSION }}"
files: "*.vsix"
generate_release_notes: true
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ Wraps the current cell in `BenchmarkTools.@benchmark` and opens a side panel wit
u = rand(1000); sort(u) # ▶ median 12.34 μs allocs 1
```

### Variable Inspector — auto-opens after every Run Block
After every block execution, a side panel pops open showing all live variables in `Main` — name, type, size, and a value preview. No extra command needed.

| Name | Type | Size | Value |
|---|---|---|---|
| `sol` | `ODESolution` | `scalar` | `retcode: Success` |
| `u0` | `Vector{Float64}` | `(3,)` | `[1.0, 0.0, 0.0]` |

### Plot Viewer — auto-opens on plot output
If your block calls `plot(...)` via **Plots.jl** or **CairoMakie**, the output PNG is captured and displayed in a side panel automatically. Re-run the block to refresh it.

### SciML Snippets
Type a prefix and press `Tab` to expand full boilerplate:

| Prefix | Expands to |
|---|---|
| `ode` | `ODEProblem` + `solve` with Tsit5 |
| `sde` | `SDEProblem` with drift + diffusion |
| `mtk` | `ModelingToolkit` `@variables` + `@equations` system |
| `neural_ode` | NeuralODE with Lux.jl |
| `bvp` | `BVProblem` with boundary conditions |
| `optim` | `OptimizationProblem` with Optim.jl |
| `pinn` | Physics-Informed NN via NeuralPDE.jl |
| `diffeq` | Full define → solve → plot workflow |

### Equation Preview — `Ctrl+Shift+M`
Opens a live-updating side panel that renders all LaTeX math found in the file using [KaTeX](https://katex.org). Supports all three SciML/Documenter.jl formats:

Expand Down
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "sciml-toolkit",
"displayName": "SciML Toolkit",
"description": "Julia SciML toolkit: equation preview, inline code execution, and benchmark runner",
"version": "0.1.1",
"description": "Julia SciML toolkit: inline execution, variable inspector, plot viewer, benchmarks, equation preview, and snippets",
"version": "0.2.0",
"publisher": "harsh",
"license": "MIT",
"repository": {
Expand All @@ -11,7 +11,7 @@
},
"homepage": "https://github.com/singhharsh1708/sciml-toolkit#readme",
"bugs": { "url": "https://github.com/singhharsh1708/sciml-toolkit/issues" },
"keywords": ["julia", "sciml", "differential equations", "benchmarks", "latex", "math"],
"keywords": ["julia", "sciml", "differential equations", "benchmarks", "latex", "math", "plots", "snippets"],
"engines": { "vscode": "^1.95.0" },
"categories": ["Other", "Notebooks", "Visualization"],
"activationEvents": ["onLanguage:julia"],
Expand All @@ -36,6 +36,11 @@
{
"command": "sciml.clearOutputs",
"title": "SciML: Clear All Outputs"
},
{
"command": "sciml.showVariables",
"title": "SciML: Show Variable Inspector",
"icon": "$(table)"
}
],
"menus": {
Expand All @@ -54,6 +59,11 @@
"command": "sciml.previewEquations",
"when": "resourceLangId == julia",
"group": "sciml@3"
},
{
"command": "sciml.showVariables",
"when": "resourceLangId == julia",
"group": "sciml@4"
}
]
},
Expand Down
11 changes: 10 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import * as vscode from 'vscode';
import { JuliaRunner } from './juliaRunner';
import { BenchmarkPanel } from './benchmarkPanel';
import { EquationPreview } from './equationPreview';
import { registerSnippets } from './snippets';
import { VariableInspector } from './variableInspector';

let runner: JuliaRunner | undefined;

export function activate(context: vscode.ExtensionContext) {
runner = new JuliaRunner();

// Register SciML snippet completions
registerSnippets(context);

context.subscriptions.push(
vscode.commands.registerCommand('sciml.runBlock', () => {
const editor = vscode.window.activeTextEditor;
if (editor) runner?.runBlock(editor);
if (editor) runner?.runBlock(editor, context);
}),

vscode.commands.registerCommand('sciml.runBenchmark', () => {
Expand All @@ -26,6 +31,10 @@ export function activate(context: vscode.ExtensionContext) {

vscode.commands.registerCommand('sciml.clearOutputs', () => {
runner?.clearDecorations();
}),

vscode.commands.registerCommand('sciml.showVariables', () => {
VariableInspector.show(context, []);
})
);
}
Expand Down
27 changes: 22 additions & 5 deletions src/juliaRunner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as vscode from 'vscode';
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
import { BenchmarkPanel, buildBenchmarkScript, parseBenchmarkOutput } from './benchmarkPanel';
import { VariableInspector, VAR_INSPECT_SUFFIX, parseVariables } from './variableInspector';
import { PlotViewer, buildPlotSuffix, tempPlotPath, parsePlotPath } from './plotViewer';

// ─── Types ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -62,7 +64,7 @@ export class JuliaRunner {

// ─── Public commands ────────────────────────────────────────────────────────

async runBlock(editor: vscode.TextEditor) {
async runBlock(editor: vscode.TextEditor, context: vscode.ExtensionContext) {
const { code, endLine } = this.getBlock(editor);
if (!code.trim()) {
void vscode.window.showInformationMessage('SciML: cursor is not inside a code block.');
Expand All @@ -71,18 +73,33 @@ export class JuliaRunner {

this.setRunning(editor, endLine);
try {
// Prefer the julia-vscode REPL if available; fall back to subprocess
const plotPath = tempPlotPath();
// Append variable inspector + plot capture suffixes
const fullCode = code + '\n' + VAR_INSPECT_SUFFIX + '\n' + buildPlotSuffix(plotPath);

const config = vscode.workspace.getConfiguration('sciml');
const useRepl: boolean = config.get('useExistingRepl') ?? true;
let output: string;

if (useRepl && (await this.juliaVscodeReplAvailable())) {
output = await this.runViaRepl(code);
output = await this.runViaRepl(fullCode);
} else {
output = await this.execJulia(code);
output = await this.execJulia(fullCode);
}

this.showResult(editor, endLine, output, false);
// Show first meaningful output line inline
const displayLine = output.split('\n')
.find((l) => l.trim() && !l.startsWith('__sciml_'));
this.showResult(editor, endLine, displayLine ?? '(no output)', false);

// Update variable inspector panel
const vars = parseVariables(output);
VariableInspector.show(context, vars);

// Show plot if one was saved
const pPath = parsePlotPath(output);
if (pPath) PlotViewer.show(context, pPath);

} catch (err: unknown) {
this.showResult(editor, endLine, errorMessage(err), true);
}
Expand Down
147 changes: 147 additions & 0 deletions src/plotViewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

// Sentinel emitted by the Julia suffix when a plot was saved
export const PLOT_SENTINEL = '__sciml_plot__:';

// ─── Julia code appended after each block run ────────────────────────────────
// Tries Plots.jl first, then Makie (CairoMakie). Writes PNG to a temp file.

export function buildPlotSuffix(outPath: string): string {
return `
let
__path = raw"${outPath}"
__saved = false

# Try Plots.jl
if !__saved
try
if isdefined(Main, :Plots) && Plots.current() !== nothing
Plots.savefig(__path)
__saved = true
end
catch _
end
end

# Try CairoMakie / GLMakie current figure
if !__saved
try
if isdefined(Main, :CairoMakie)
CairoMakie.save(__path, CairoMakie.current_figure())
__saved = true
elseif isdefined(Main, :Makie)
Makie.save(__path, Makie.current_figure())
__saved = true
end
catch _
end
end

if __saved
println("${PLOT_SENTINEL}", __path)
end
end
`;
}

// ─── Panel ───────────────────────────────────────────────────────────────────

export class PlotViewer {
private static current: PlotViewer | undefined;
private readonly panel: vscode.WebviewPanel;
private readonly context: vscode.ExtensionContext;

private constructor(panel: vscode.WebviewPanel, context: vscode.ExtensionContext) {
this.panel = panel;
this.context = context;
panel.onDidDispose(() => { PlotViewer.current = undefined; });
}

static show(context: vscode.ExtensionContext, imagePath: string) {
if (!fs.existsSync(imagePath)) return;

const panel = PlotViewer.current?.panel ?? vscode.window.createWebviewPanel(
'scimlPlot',
'SciML Plot',
{ viewColumn: vscode.ViewColumn.Two, preserveFocus: true },
{
enableScripts: false,
localResourceRoots: [
vscode.Uri.file(path.dirname(imagePath)),
context.extensionUri,
],
retainContextWhenHidden: true,
}
);

if (!PlotViewer.current) {
PlotViewer.current = new PlotViewer(panel, context);
} else {
PlotViewer.current.panel.reveal(vscode.ViewColumn.Two, true);
}

PlotViewer.current.update(imagePath);
}

private update(imagePath: string) {
const nonce = crypto.randomBytes(16).toString('hex');
const imgUri = this.panel.webview.asWebviewUri(vscode.Uri.file(imagePath));
this.panel.webview.html = buildHtml(imgUri.toString(), nonce, this.panel.webview.cspSource);
this.panel.title = `SciML Plot · ${path.basename(imagePath)}`;
}
}

// ─── Temp path helper ─────────────────────────────────────────────────────────

export function tempPlotPath(): string {
return path.join(os.tmpdir(), `sciml_plot_${Date.now()}.png`);
}

// Parse the sentinel from Julia stdout
export function parsePlotPath(raw: string): string | undefined {
const m = raw.match(/__sciml_plot__:(.+)/);
return m ? m[1].trim() : undefined;
}

// ─── HTML ─────────────────────────────────────────────────────────────────────

function buildHtml(imgUri: string, nonce: string, cspSource: string): string {
return /* html */`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
style-src ${cspSource} 'unsafe-inline';
img-src ${cspSource} vscode-resource:;">
<title>SciML Plot</title>
<style>
html, body { margin: 0; padding: 0; background: var(--vscode-editor-background);
display: flex; flex-direction: column; height: 100vh; }
.toolbar { padding: 6px 12px; background: var(--vscode-editor-inactiveSelectionBackground);
font-family: var(--vscode-font-family); font-size: 0.8em;
color: var(--vscode-descriptionForeground); display: flex;
align-items: center; gap: 12px; flex-shrink: 0; }
.hint { font-style: italic; }
.img-wrap { flex: 1; overflow: auto; display: flex;
align-items: center; justify-content: center; padding: 16px; }
img { max-width: 100%; max-height: 100%; object-fit: contain;
border-radius: 4px; box-shadow: 0 2px 12px rgba(0,0,0,0.4); }
</style>
</head>
<body>
<div class="toolbar">
<span>📈 SciML Plot</span>
<span class="hint">Re-run the block to refresh</span>
</div>
<div class="img-wrap">
<img src="${imgUri}" alt="Julia plot output">
</div>
</body>
</html>`;
}
Loading
Loading