diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..701d8ac --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index d46b31e..3913edc 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/package.json b/package.json index b3d1d12..2272e41 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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"], @@ -36,6 +36,11 @@ { "command": "sciml.clearOutputs", "title": "SciML: Clear All Outputs" + }, + { + "command": "sciml.showVariables", + "title": "SciML: Show Variable Inspector", + "icon": "$(table)" } ], "menus": { @@ -54,6 +59,11 @@ "command": "sciml.previewEquations", "when": "resourceLangId == julia", "group": "sciml@3" + }, + { + "command": "sciml.showVariables", + "when": "resourceLangId == julia", + "group": "sciml@4" } ] }, diff --git a/src/extension.ts b/src/extension.ts index e66f56d..be87abd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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', () => { @@ -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, []); }) ); } diff --git a/src/juliaRunner.ts b/src/juliaRunner.ts index 19fcf72..b25cd9b 100644 --- a/src/juliaRunner.ts +++ b/src/juliaRunner.ts @@ -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 ──────────────────────────────────────────────────────────────────── @@ -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.'); @@ -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); } diff --git a/src/plotViewer.ts b/src/plotViewer.ts new file mode 100644 index 0000000..40c7114 --- /dev/null +++ b/src/plotViewer.ts @@ -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 */` + + + + + + SciML Plot + + + +
+ 📈 SciML Plot + Re-run the block to refresh +
+
+ Julia plot output +
+ +`; +} diff --git a/src/snippets.ts b/src/snippets.ts new file mode 100644 index 0000000..025d51d --- /dev/null +++ b/src/snippets.ts @@ -0,0 +1,231 @@ +import * as vscode from 'vscode'; + +// ─── Snippet definitions ────────────────────────────────────────────────────── +// Each entry: trigger prefix → { label, detail, body (snippet string) } + +const SCIML_SNIPPETS: Array<{ + prefix: string; + label: string; + detail: string; + body: string; +}> = [ + { + prefix: 'ode', + label: 'ODE Problem (SciML)', + detail: 'ODEProblem + solve with Tsit5', + body: `using DifferentialEquations + +function \${1:f}!(du, u, p, t) + \${2:# du[1] = ...} +end + +u0 = [\${3:1.0}] +tspan = (\${4:0.0}, \${5:10.0}) +p = \${6:()} +prob = ODEProblem(\${1:f}!, u0, tspan, p) +sol = solve(prob, Tsit5()) +\${0}`, + }, + { + prefix: 'sde', + label: 'SDE Problem (SciML)', + detail: 'SDEProblem with drift + diffusion', + body: `using DifferentialEquations + +function \${1:drift}!(du, u, p, t) + \${2:du[1] = -u[1]} +end + +function \${3:diffusion}!(du, u, p, t) + \${4:du[1] = 0.1} +end + +u0 = [\${5:1.0}] +tspan = (\${6:0.0}, \${7:10.0}) +prob = SDEProblem(\${1:drift}!, \${3:diffusion}!, u0, tspan) +sol = solve(prob, SOSRI()) +\${0}`, + }, + { + prefix: 'mtk', + label: 'ModelingToolkit System', + detail: 'ODESystem via @variables + @equations', + body: `using ModelingToolkit, DifferentialEquations + +@variables t \${1:x}(t) \${2:y}(t) +@parameters \${3:σ}=10 \${4:ρ}=28 \${5:β}=8/3 +D = Differential(t) + +eqs = [ + D(\${1:x}) ~ \${6:\${3:σ} * (\${2:y} - \${1:x})}, + D(\${2:y}) ~ \${7:\${1:x} * (\${4:ρ} - \${1:x}) - \${2:y}}, +] + +@named sys = ODESystem(eqs, t) +sys = structural_simplify(sys) + +u0 = [\${1:x} => \${8:1.0}, \${2:y} => \${9:0.0}] +tspan = (\${10:0.0}, \${11:10.0}) +prob = ODEProblem(sys, u0, tspan) +sol = solve(prob, Tsit5()) +\${0}`, + }, + { + prefix: 'neural_ode', + label: 'Neural ODE (Lux + DiffEq)', + detail: 'NeuralODE with Lux.jl and Optimisers', + body: `using DifferentialEquations, Lux, Optimisers, Zygote, Random + +rng = Random.default_rng() + +# Define neural network +nn = Lux.Chain( + Lux.Dense(\${1:2}, \${2:32}, tanh), + Lux.Dense(\${2:32}, \${1:2}), +) +ps, st = Lux.setup(rng, nn) + +# ODE via neural network +function neural_f(u, p, t) + nn(u, p, st)[1] +end + +u0 = \${3:Float32[1.0, 0.0]} +tspan = (\${4:0.0f0}, \${5:2.0f0}) +prob = ODEProblem(neural_f, u0, tspan, ps) + +sol = solve(prob, Tsit5(), saveat=\${6:0.1}) +\${0}`, + }, + { + prefix: 'bvp', + label: 'BVP Problem (SciML)', + detail: 'BVProblem with boundary conditions', + body: `using BoundaryValueDiffEq + +function \${1:bvp_f}!(du, u, p, t) + \${2:du[1] = u[2] + du[2] = -u[1]} +end + +function \${3:bc}!(residual, u, p, t) + \${4:residual[1] = u[1][1] # u(0) = 0 + residual[2] = u[end][1] - 1 # u(1) = 1} +end + +tspan = (\${5:0.0}, \${6:1.0}) +prob = BVProblem(\${1:bvp_f}!, \${3:bc}!, \${7:[0.0, 1.0]}, tspan) +sol = solve(prob, MIRK4(), dt=\${8:0.05}) +\${0}`, + }, + { + prefix: 'optim', + label: 'Optimization Problem (SciML)', + detail: 'OptimizationProblem with Optim.jl backend', + body: `using Optimization, OptimizationOptimJL + +function \${1:loss}(u, p) + \${2:sum(abs2, u)} +end + +u0 = \${3:zeros(2)} +p = \${4:nothing} +prob = OptimizationProblem(\${1:loss}, u0, p) +sol = solve(prob, \${5:LBFGS}()) +println("Minimum: ", sol.u, " → ", sol.objective) +\${0}`, + }, + { + prefix: 'pinn', + label: 'Physics-Informed NN (NeuralPDE)', + detail: 'PINN for a 1-D PDE via NeuralPDE.jl', + body: `using NeuralPDE, ModelingToolkit, Lux, Optimisers + +@variables t x u(..) +Dt = Differential(t); Dx = Differential(x); Dxx = Differential(x)^2 + +# PDE: ∂u/∂t = κ ∂²u/∂x² +\${1:κ} = \${2:0.1} +eq = Dt(u(t,x)) ~ \${1:κ} * Dxx(u(t,x)) + +bcs = [ + u(0, x) ~ \${3:sin(π*x)}, + u(t, 0) ~ 0.0, + u(t, 1) ~ 0.0, +] + +domains = [t ∈ (0.0, \${4:1.0}), x ∈ (0.0, 1.0)] + +chain = Lux.Chain(Lux.Dense(2, \${5:16}, tanh), Lux.Dense(\${5:16}, 1)) +discretization = PhysicsInformedNN(chain, GridTraining(\${6:0.1})) + +@named pde_sys = PDESystem(eq, bcs, domains, [t,x], [u(t,x)]) +prob = discretize(pde_sys, discretization) +sol = solve(prob, \${7:Adam(0.01)}, maxiters=\${8:1000}) +\${0}`, + }, + { + prefix: 'diffeq', + label: 'Full DiffEq Workflow', + detail: 'Define, solve, and plot an ODE system', + body: `using DifferentialEquations, Plots + +# ── System ────────────────────────────────────────── +function \${1:system}!(du, u, p, t) + \${2:a}, \${3:b} = p + \${4:du[1] = \${2:a} * u[1] + du[2] = \${3:b} * u[2]} +end + +# ── Setup ──────────────────────────────────────────── +u0 = [\${5:1.0, 0.5}] +tspan = (\${6:0.0}, \${7:10.0}) +p = (\${8:-0.5, -1.0}) +prob = ODEProblem(\${1:system}!, u0, tspan, p) + +# ── Solve ──────────────────────────────────────────── +sol = solve(prob, \${9:Tsit5}(), saveat=\${10:0.1}) +println("Final state: ", sol.u[end]) + +# ── Plot ───────────────────────────────────────────── +plot(sol, xlabel="t", ylabel="u", title="\${1:system}") +\${0}`, + }, +]; + +// ─── Provider ──────────────────────────────────────────────────────────────── + +export class SciMLCompletionProvider implements vscode.CompletionItemProvider { + provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position + ): vscode.CompletionItem[] { + const linePrefix = document.lineAt(position).text.slice(0, position.character); + + // Only trigger on a fresh word (not inside a string or comment in most cases) + const word = linePrefix.match(/\b(\w+)$/)?.[1] ?? ''; + if (!word) return []; + + return SCIML_SNIPPETS + .filter((s) => s.prefix.startsWith(word)) + .map((s) => { + const item = new vscode.CompletionItem(s.prefix, vscode.CompletionItemKind.Snippet); + item.label = s.prefix; + item.detail = s.label; + item.documentation = new vscode.MarkdownString(`**${s.label}**\n\n${s.detail}`); + item.insertText = new vscode.SnippetString(s.body); + item.sortText = `0_${s.prefix}`; // float to top of list + return item; + }); + } +} + +export function registerSnippets(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + { language: 'julia' }, + new SciMLCompletionProvider(), + // trigger on any letter — VS Code calls us on normal typing too + ) + ); +} diff --git a/src/variableInspector.ts b/src/variableInspector.ts new file mode 100644 index 0000000..269dfea --- /dev/null +++ b/src/variableInspector.ts @@ -0,0 +1,136 @@ +import * as vscode from 'vscode'; +import * as crypto from 'crypto'; + +export interface Variable { + name: string; + type: string; + size: string; + preview: string; +} + +export class VariableInspector { + private static current: VariableInspector | undefined; + private readonly panel: vscode.WebviewPanel; + + private constructor(panel: vscode.WebviewPanel) { + this.panel = panel; + panel.onDidDispose(() => { VariableInspector.current = undefined; }); + } + + static show(context: vscode.ExtensionContext, vars: Variable[]) { + if (VariableInspector.current) { + VariableInspector.current.panel.reveal(vscode.ViewColumn.Two, true); + VariableInspector.current.update(vars); + return; + } + + const panel = vscode.window.createWebviewPanel( + 'scimlVars', + 'SciML Variables', + { viewColumn: vscode.ViewColumn.Two, preserveFocus: true }, + { + enableScripts: true, + localResourceRoots: [context.extensionUri], + retainContextWhenHidden: true, + } + ); + + VariableInspector.current = new VariableInspector(panel); + VariableInspector.current.update(vars); + } + + private update(vars: Variable[]) { + const nonce = crypto.randomBytes(16).toString('hex'); + this.panel.webview.html = buildHtml(vars, nonce, this.panel.webview.cspSource); + } +} + +// ─── Julia code injected after each block run ──────────────────────────────── +// Emits all non-core Main bindings in a delimited format. + +export const VAR_INSPECT_SUFFIX = ` +let + __skip = Set([:Base, :Core, :Main, :InteractiveUtils, :ans]) + println("__sciml_vars_start__") + for __name in names(Main, all=false, imported=false) + __name in __skip && continue + try + __val = getfield(Main, __name) + __type = string(typeof(__val)) + __sz = try; string(size(__val)); catch _; "scalar"; end + __prev = try; repr(__val)[1:min(80, length(repr(__val)))]; catch _; "?"; end + println(__name, "|||", __type, "|||", __sz, "|||", __prev) + catch _ + end + end + println("__sciml_vars_end__") +end +`; + +export function parseVariables(raw: string): Variable[] { + const block = raw.match(/__sciml_vars_start__\n([\s\S]*?)__sciml_vars_end__/); + if (!block) return []; + return block[1] + .trim() + .split('\n') + .filter(Boolean) + .map((line) => { + const [name, type, size, ...rest] = line.split('|||'); + return { name: name ?? '?', type: type ?? '?', size: size ?? '?', preview: rest.join('|||') }; + }); +} + +// ─── WebView ────────────────────────────────────────────────────────────────── + +function buildHtml(vars: Variable[], nonce: string, cspSource: string): string { + const rows = vars.length + ? vars.map((v) => /* html */` + + ${escHtml(v.name)} + ${escHtml(v.type)} + ${escHtml(v.size)} + ${escHtml(v.preview)} + `).join('') + : `No variables — run a block first.`; + + return /* html */` + + + + + + SciML Variables + + + +

Variables (${vars.length})

+ + + ${rows} +
NameTypeSizeValue
+ +`; +} + +function escHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>'); +}