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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "sciml-toolkit",
"displayName": "SciML Toolkit",
"description": "Julia SciML toolkit: inline execution, variable inspector, plot viewer, benchmarks, equation preview, and snippets",
"version": "0.2.0",
"version": "0.3.0",
"publisher": "harsh",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -41,6 +41,11 @@
"command": "sciml.showVariables",
"title": "SciML: Show Variable Inspector",
"icon": "$(table)"
},
{
"command": "sciml.restartRepl",
"title": "SciML: Restart Julia Session",
"icon": "$(refresh)"
}
],
"menus": {
Expand Down
74 changes: 74 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,51 @@ import { BenchmarkPanel } from './benchmarkPanel';
import { EquationPreview } from './equationPreview';
import { registerSnippets } from './snippets';
import { VariableInspector } from './variableInspector';
import { PackageManager } from './packageManager';
import { PlotViewer, tempPlotPath } from './plotViewer';

let runner: JuliaRunner | undefined;
let pkgManager: PackageManager | undefined;

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

// Register SciML snippet completions
registerSnippets(context);

// Start REPL session eagerly when a Julia file is open
const startSessionIfNeeded = () => {
if (!runner!.session.isReady()) {
void runner!.session.start().then(() => {
// Check packages once session is ready
const editor = vscode.window.activeTextEditor;
if (editor?.document.languageId === 'julia') {
void pkgManager!.checkAndOffer(editor.document);
}
}).catch(() => { /* Julia not found — commands will fall back to subprocess */ });
}
};

// Start when any Julia file is opened or becomes active
if (vscode.window.activeTextEditor?.document.languageId === 'julia') {
startSessionIfNeeded();
}

context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor?.document.languageId === 'julia') {
startSessionIfNeeded();
void pkgManager!.checkAndOffer(editor.document);
}
}),

vscode.workspace.onDidSaveTextDocument((doc) => {
if (doc.languageId === 'julia') {
void pkgManager!.checkAndOffer(doc);
}
}),

vscode.commands.registerCommand('sciml.runBlock', () => {
const editor = vscode.window.activeTextEditor;
if (editor) runner?.runBlock(editor, context);
Expand All @@ -35,6 +70,45 @@ export function activate(context: vscode.ExtensionContext) {

vscode.commands.registerCommand('sciml.showVariables', () => {
VariableInspector.show(context, []);
}),

vscode.commands.registerCommand('sciml.plotVariable', async (varName: string) => {
if (!runner!.session.isReady()) {
void vscode.window.showWarningMessage('SciML: no active Julia session — run a block first.');
return;
}
const plotPath = tempPlotPath();
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
status.text = `$(sync~spin) SciML: plotting ${varName}…`;
status.show();
try {
const code = `
import Plots
Plots.plot(${varName})
Plots.savefig(raw"${plotPath}")
println("__sciml_plot__:${plotPath}")
`;
await runner!.session.exec(code, 30_000);
PlotViewer.show(context, plotPath);
} catch (err) {
void vscode.window.showErrorMessage(`SciML: plot failed — ${err}`);
} finally {
status.dispose();
}
}),

vscode.commands.registerCommand('sciml.restartRepl', async () => {
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
status.text = '$(sync~spin) SciML: Restarting Julia session…';
status.show();
try {
await runner!.session.restart();
void vscode.window.showInformationMessage('SciML: Julia session restarted.');
} catch (err) {
void vscode.window.showErrorMessage(`SciML: restart failed — ${err}`);
} finally {
status.dispose();
}
})
);
}
Expand Down
18 changes: 9 additions & 9 deletions src/juliaRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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';
import { ReplSession } from './replSession';

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

Expand All @@ -26,6 +27,7 @@ export class JuliaRunner {
private outputDec: vscode.TextEditorDecorationType;
private errorDec: vscode.TextEditorDecorationType;
private runningDec: vscode.TextEditorDecorationType;
readonly session: ReplSession = new ReplSession();

constructor() {
this.outputDec = vscode.window.createTextEditorDecorationType({
Expand Down Expand Up @@ -74,29 +76,26 @@ export class JuliaRunner {
this.setRunning(editor, endLine);
try {
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(fullCode);
// Prefer the persistent REPL session (instant — no startup cost).
// Fall back to one-shot subprocess if session isn't ready yet.
if (this.session.isReady()) {
output = await this.session.exec(fullCode);
} else {
// Try to start session in background for next run
void this.session.start().catch(() => { /* will retry next run */ });
output = await this.execJulia(fullCode);
}

// 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);

Expand Down Expand Up @@ -147,6 +146,7 @@ export class JuliaRunner {
this.outputDec.dispose();
this.errorDec.dispose();
this.runningDec.dispose();
this.session.dispose();
}

// ─── Block detection ────────────────────────────────────────────────────────
Expand Down
102 changes: 102 additions & 0 deletions src/packageManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as vscode from 'vscode';
import { ReplSession } from './replSession';

// ─── Package auto-installer ───────────────────────────────────────────────────
// Scans the active document for `using X` / `import X` statements,
// checks which packages are missing from the Julia environment,
// and shows a notification offering to install them in one click.

const USING_RE = /^\s*(?:using|import)\s+([\w.]+(?:\s*,\s*[\w.]+)*)/gm;

// Packages that ship with Julia base — no install needed
const STDLIB = new Set([
'Base', 'Core', 'Main', 'Dates', 'LinearAlgebra', 'Random', 'Statistics',
'Printf', 'Test', 'REPL', 'Logging', 'Pkg', 'InteractiveUtils', 'Markdown',
'Serialization', 'Sockets', 'Profile', 'FileWatching', 'SHA', 'Unicode',
'DelimitedFiles', 'SparseArrays', 'SharedArrays', 'Mmap', 'LibGit2',
]);

export function extractUsedPackages(text: string): string[] {
const pkgs = new Set<string>();
for (const m of text.matchAll(USING_RE)) {
for (const raw of m[1].split(',')) {
const pkg = raw.trim().split('.')[0]; // `Plots.jl` → `Plots`
if (pkg && !STDLIB.has(pkg)) pkgs.add(pkg);
}
}
return [...pkgs];
}

export class PackageManager {
private session: ReplSession;
private lastChecked = new Map<string, number>(); // pkg → timestamp
private RECHECK_MS = 5 * 60 * 1000; // re-check at most once per 5 min

constructor(session: ReplSession) {
this.session = session;
}

// Call after opening / saving a Julia file
async checkAndOffer(document: vscode.TextDocument): Promise<void> {
if (!this.session.isReady()) return;

const pkgs = extractUsedPackages(document.getText());
if (pkgs.length === 0) return;

// Filter out recently-checked packages
const now = Date.now();
const toCheck = pkgs.filter(
(p) => !this.lastChecked.has(p) || now - this.lastChecked.get(p)! > this.RECHECK_MS
);
if (toCheck.length === 0) return;

// Ask Julia which ones are actually missing
const checkCode = `__sciml_pkg_status__(${JSON.stringify(toCheck)})`;
let output: string;
try {
output = await this.session.exec(checkCode, 10_000);
} catch {
return;
}

const missing = output
.split('\n')
.filter((l) => l.startsWith('__sciml_missing__:'))
.map((l) => l.replace('__sciml_missing__:', '').trim());

// Mark all checked
for (const p of toCheck) this.lastChecked.set(p, now);

if (missing.length === 0) return;

const label = missing.length === 1
? `Install ${missing[0]}`
: `Install ${missing.length} packages (${missing.join(', ')})`;

const choice = await vscode.window.showInformationMessage(
`SciML: missing Julia packages: ${missing.join(', ')}`,
label,
'Ignore'
);

if (choice !== label) return;

const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
status.text = `$(sync~spin) Installing ${missing.join(', ')}…`;
status.show();

try {
const installCode = `import Pkg; Pkg.add(${JSON.stringify(missing)})`;
await this.session.exec(installCode, 300_000);
void vscode.window.showInformationMessage(
`SciML: installed ${missing.join(', ')} successfully.`
);
} catch (err) {
void vscode.window.showErrorMessage(
`SciML: package install failed — ${err instanceof Error ? err.message : String(err)}`
);
} finally {
status.dispose();
}
}
}
Loading
Loading