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 */` + +
+ + + +| Name | Type | Size | Value |
|---|