From cbab34bd2c405ee223d2c9d556063b682ea186d6 Mon Sep 17 00:00:00 2001 From: mdsw Date: Fri, 18 Jul 2025 00:23:00 -0400 Subject: [PATCH 1/5] initial code --- README.md | 2 +- package-lock.json | 8 +++---- src/RunButton.ts | 1 + src/main.ts | 2 +- src/settings/Settings.ts | 10 +++++++++ src/settings/SettingsTab.ts | 2 ++ src/settings/languageDisplayName.ts | 1 + src/settings/per-lang/makeJuliaSettings.ts | 26 ++++++++++++++++++++++ 8 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/settings/per-lang/makeJuliaSettings.ts diff --git a/README.md b/README.md index ab309ce2..51c2c086 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The result is shown only after the execution is finished. It is not possible to
-The following [languages are supported](#supported-programming-languages-): C, C++, CSharp, Dart, F#, Golang, Groovy, Haskell, Java, JavaScript, Kotlin, Lean, Lua, Maxima, OCaml, Octave, Prolog, Python, R, Racket, Ruby, Rust, Scala, Shell (including Batch & Powershell), SQL, TypeScript, Wolfram Mathematica, Zig. +The following [languages are supported](#supported-programming-languages-): C, C++, CSharp, Dart, F#, Golang, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lean, Lua, Maxima, OCaml, Octave, Prolog, Python, R, Racket, Ruby, Rust, Scala, Shell (including Batch & Powershell), SQL, TypeScript, Wolfram Mathematica, Zig. If you are new to MarkDown or Obsidian.md, you can go to the [Quickstart Guide](#quickstart-guide-) or take a look in to [some blogs and videos that feature this plugin](#featured-in) diff --git a/package-lock.json b/package-lock.json index 98f37e22..5e7eb31b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,12 @@ { "name": "execute-code", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "execute-code", - "version": "2.0.0", + "version": "2.1.2", "license": "MIT", "dependencies": { "g": "^2.0.1", @@ -4242,6 +4243,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "version": "2.1.2" -} \ No newline at end of file + } +} diff --git a/src/RunButton.ts b/src/RunButton.ts index a3dae51d..d5928a76 100644 --- a/src/RunButton.ts +++ b/src/RunButton.ts @@ -76,6 +76,7 @@ async function handleExecution(block: CodeBlockContext) { case "zig": return runCode(s.zigPath, s.zigArgs, "zig", block, { shell: true }); case "ocaml": return runCode(s.ocamlPath, s.ocamlArgs, "ocaml", block, { shell: true }); case "php": return runCode(s.phpPath, s.phpArgs, s.phpFileExtension, block, { shell: true }); + case "julia": return runCode(s.juliaPath, s.juliaArgs,s.juliaFileExtension, block, {shell:true}); case "latex": const outputPath: string = await retrieveFigurePath(block.srcCode, s.latexFigureTitlePattern, block.markdownFile, s); const invokeCompiler: string = [s.latexTexfotArgs, s.latexCompilerPath, s.latexCompilerArgs].join(" "); diff --git a/src/main.ts b/src/main.ts index 6dcbb63d..31da376d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,7 @@ import * as runButton from './RunButton'; export const languageAliases = ["javascript", "typescript", "bash", "csharp", "wolfram", "nb", "wl", "hs", "py", "tex"] as const; export const canonicalLanguages = ["js", "ts", "cs", "latex", "lean", "lua", "python", "cpp", "prolog", "shell", "groovy", "r", "go", "rust", "java", "powershell", "kotlin", "mathematica", "haskell", "scala", "swift", "racket", "fsharp", "c", "dart", - "ruby", "batch", "sql", "octave", "maxima", "applescript", "zig", "ocaml", "php"] as const; + "ruby", "batch", "sql", "octave", "maxima", "applescript", "zig", "ocaml", "php", "julia"] as const; export const supportedLanguages = [...languageAliases, ...canonicalLanguages] as const; export type LanguageId = typeof canonicalLanguages[number]; diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d67e3798..6211b243 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -142,6 +142,10 @@ export interface ExecutorSettings { phpArgs: string; phpFileExtension: string; phpInject: string; + juliaPath: string; + juliaArgs: string; + juliaFileExtension: string; + juliaInject: string; scalaPath: string; scalaArgs: string; scalaFileExtension: string; @@ -210,6 +214,7 @@ export interface ExecutorSettings { zigInteractive: boolean; ocamlInteractive: boolean; phpInteractive: boolean; + juliaInteractive: boolean; } @@ -387,6 +392,10 @@ export const DEFAULT_SETTINGS: ExecutorSettings = { phpArgs: "", phpFileExtension: "php", phpInject: "", + juliaPath: "julia", + juliaArgs: "-E", + juliaFileExtension: "jl", + juliaInject: "", jsInteractive: true, tsInteractive: false, csInteractive: false, @@ -422,4 +431,5 @@ export const DEFAULT_SETTINGS: ExecutorSettings = { zigInteractive: false, ocamlInteractive: false, phpInteractive: false, + juliaInteractive: false, } diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 2b32a8eb..1d9a9c28 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -10,6 +10,7 @@ import makeGroovySettings from "./per-lang/makeGroovySettings"; import makeHaskellSettings from "./per-lang/makeHaskellSettings"; import makeJavaSettings from "./per-lang/makeJavaSettings"; import makeJsSettings from "./per-lang/makeJsSettings"; +import makeJuliaSettings from "./per-lang/makeJuliaSettings"; import makeKotlinSettings from "./per-lang/makeKotlinSettings"; import makeLatexSettings from "./per-lang/makeLatexSettings"; import makeLeanSettings from "./per-lang/makeLeanSettings"; @@ -172,6 +173,7 @@ export class SettingsTab extends PluginSettingTab { makeOCamlSettings(this, this.makeContainerFor("ocaml")); makePhpSettings(this, this.makeContainerFor("php")); makeLatexSettings(this, this.makeContainerFor("latex")); + makeJuliaSettings(this,this.makeContainerFor("julia")); this.focusContainer(this.plugin.settings.lastOpenLanguageTab || canonicalLanguages[0]); } diff --git a/src/settings/languageDisplayName.ts b/src/settings/languageDisplayName.ts index 2fc88a96..54411324 100644 --- a/src/settings/languageDisplayName.ts +++ b/src/settings/languageDisplayName.ts @@ -8,6 +8,7 @@ export const DISPLAY_NAMES: Record = { haskell: "Haskell", java: "Java", js: "Javascript", + julia: "Julia", kotlin: "Kotlin", latex: "LaTeX", lua: "Lua", diff --git a/src/settings/per-lang/makeJuliaSettings.ts b/src/settings/per-lang/makeJuliaSettings.ts new file mode 100644 index 00000000..dccdeaa6 --- /dev/null +++ b/src/settings/per-lang/makeJuliaSettings.ts @@ -0,0 +1,26 @@ +import { Setting } from "obsidian"; +import { SettingsTab } from "../SettingsTab"; + +export default (tab: SettingsTab, containerEl: HTMLElement) => { + containerEl.createEl('h3', { text: 'Julia Settings' }); + new Setting(containerEl) + .setName('Julia path') + .addText(text => text + .setValue(tab.plugin.settings.juliaPath) + .onChange(async (value) => { + const sanitized = tab.sanitizePath(value); + tab.plugin.settings.juliaPath = sanitized; + console.log('julia path set to: ' + sanitized); + await tab.plugin.saveSettings(); + })); + new Setting(containerEl) + .setName('Julia arguments') + .addText(text => text + .setValue(tab.plugin.settings.juliaArgs) + .onChange(async (value) => { + tab.plugin.settings.juliaArgs = value; + console.log('Julia args set to: ' + value); + await tab.plugin.saveSettings(); + })); + tab.makeInjectSetting(containerEl, "julia"); +} \ No newline at end of file From e90e0d3125dae60b01856f6a2fe7a92a45d82b1e Mon Sep 17 00:00:00 2001 From: mdsw Date: Fri, 18 Jul 2025 20:34:32 -0400 Subject: [PATCH 2/5] Working non-notebook mode --- src/settings/Settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 6211b243..b291fe8b 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -393,7 +393,7 @@ export const DEFAULT_SETTINGS: ExecutorSettings = { phpFileExtension: "php", phpInject: "", juliaPath: "julia", - juliaArgs: "-E", + juliaArgs: "", juliaFileExtension: "jl", juliaInject: "", jsInteractive: true, From e06c2a785e6000f3340f51473e21bdb9e5c11549 Mon Sep 17 00:00:00 2001 From: mdsw Date: Sat, 19 Jul 2025 19:12:52 -0400 Subject: [PATCH 3/5] Julia notebook mode --- src/ExecutorContainer.ts | 4 ++- src/RunButton.ts | 2 +- src/executors/JuliaExecutor.ts | 37 ++++++++++++++++++++++ src/settings/Settings.ts | 2 +- src/settings/per-lang/makeJuliaSettings.ts | 8 +++++ 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/executors/JuliaExecutor.ts diff --git a/src/ExecutorContainer.ts b/src/ExecutorContainer.ts index 58e8b56d..e569f7fc 100644 --- a/src/ExecutorContainer.ts +++ b/src/ExecutorContainer.ts @@ -10,11 +10,13 @@ import RExecutor from "./executors/RExecutor.js"; import CExecutor from "./executors/CExecutor"; import FSharpExecutor from "./executors/FSharpExecutor"; import LatexExecutor from "./executors/LatexExecutor"; +import JuliaExecutor from "./executors/JuliaExecutor"; const interactiveExecutors: Partial> = { "js": NodeJSExecutor, "python": PythonExecutor, - "r": RExecutor + "r": RExecutor, + "julia": JuliaExecutor }; const nonInteractiveExecutors: Partial> = { diff --git a/src/RunButton.ts b/src/RunButton.ts index d5928a76..04d7b147 100644 --- a/src/RunButton.ts +++ b/src/RunButton.ts @@ -76,7 +76,7 @@ async function handleExecution(block: CodeBlockContext) { case "zig": return runCode(s.zigPath, s.zigArgs, "zig", block, { shell: true }); case "ocaml": return runCode(s.ocamlPath, s.ocamlArgs, "ocaml", block, { shell: true }); case "php": return runCode(s.phpPath, s.phpArgs, s.phpFileExtension, block, { shell: true }); - case "julia": return runCode(s.juliaPath, s.juliaArgs,s.juliaFileExtension, block, {shell:true}); + case "julia": return runCode(s.juliaPath, s.juliaArgs,s.juliaFileExtension, block); case "latex": const outputPath: string = await retrieveFigurePath(block.srcCode, s.latexFigureTitlePattern, block.markdownFile, s); const invokeCompiler: string = [s.latexTexfotArgs, s.latexCompilerPath, s.latexCompilerArgs].join(" "); diff --git a/src/executors/JuliaExecutor.ts b/src/executors/JuliaExecutor.ts new file mode 100644 index 00000000..533386aa --- /dev/null +++ b/src/executors/JuliaExecutor.ts @@ -0,0 +1,37 @@ +import {ChildProcessWithoutNullStreams} from "child_process"; +import {ExecutorSettings} from "src/settings/Settings"; +import ReplExecutor from "./ReplExecutor.js"; + + +export default class JuliaExecutor extends ReplExecutor { + + process: ChildProcessWithoutNullStreams + + constructor(settings: ExecutorSettings, file: string) { + const args = settings.juliaArgs ? settings.juliaArgs.split(" ") : []; + + args.unshift(`-i`); + + super(settings, settings.juliaPath, args, file, "julia"); + } + + /** + * Writes a single newline to ensure that the stdin is set up correctly. + */ + async setup() { + this.process.stdin.write("\n"); + } + + wrapCode(code: string, finishSigil: string): string { + + //apparently some issue with Meta.parse: https://github.com/JuliaInterop/JuliaCall/issues/197 + //return `try \n eval(Meta.parse(${JSON.stringify(code)}))\n catch err; \n bt = catch_backtrace(); msg = sprint(showerror, err, bt); println(stderr,msg) \n finally \n print(${JSON.stringify(finishSigil)}) \n end\n` + // this works and will return an error if there is one, just not on stderr + return `${code} \n print(${JSON.stringify(finishSigil)}) \n` + } + + removePrompts(output: string, source: "stdout" | "stderr"): string { + return output; + } + +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index b291fe8b..c2c22aca 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -431,5 +431,5 @@ export const DEFAULT_SETTINGS: ExecutorSettings = { zigInteractive: false, ocamlInteractive: false, phpInteractive: false, - juliaInteractive: false, + juliaInteractive: true, } diff --git a/src/settings/per-lang/makeJuliaSettings.ts b/src/settings/per-lang/makeJuliaSettings.ts index dccdeaa6..343c8734 100644 --- a/src/settings/per-lang/makeJuliaSettings.ts +++ b/src/settings/per-lang/makeJuliaSettings.ts @@ -22,5 +22,13 @@ export default (tab: SettingsTab, containerEl: HTMLElement) => { console.log('Julia args set to: ' + value); await tab.plugin.saveSettings(); })); + new Setting(containerEl) + .setName("Run Julia blocks in Notebook Mode") + .addToggle((toggle) => toggle + .setValue(tab.plugin.settings.juliaInteractive) + .onChange(async (value) => { + tab.plugin.settings.juliaInteractive = value; + await tab.plugin.saveSettings(); + })); tab.makeInjectSetting(containerEl, "julia"); } \ No newline at end of file From ebadde0ce78301d0e61bc6f7b7b1bc1e067bb59d Mon Sep 17 00:00:00 2001 From: mdsw Date: Sat, 19 Jul 2025 21:51:35 -0400 Subject: [PATCH 4/5] Julia embedded plots and README updates --- README.md | 36 ++++++++++++++++++++-- src/RunButton.ts | 2 +- src/executors/JuliaExecutor.ts | 2 +- src/settings/Settings.ts | 2 ++ src/settings/per-lang/makeJuliaSettings.ts | 8 +++++ src/transforms/Magic.ts | 15 +++++++++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 51c2c086..152a1832 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The following [languages are supported](#supported-programming-languages-): C, C If you are new to MarkDown or Obsidian.md, you can go to the [Quickstart Guide](#quickstart-guide-) or take a look in to [some blogs and videos that feature this plugin](#featured-in) -Python, R, and Octave support embedded plots. All languages support ["magic" commands](#magic-commands-) that help you to access paths in obsidian or show images in your notes. +Python, R, Julia, and Octave support embedded plots. All languages support ["magic" commands](#magic-commands-) that help you to access paths in obsidian or show images in your notes. You can create code blocks that are executed before or after each code block of the same language and define [global code injections](#global-code-injection-and-reusing-code-blocks-). @@ -137,6 +137,38 @@ plot(x, y, type="l") ``` +
+Julia + +- Requirements: Julia is installed and the correct path is set in the settings. Recommend using [juliaup](https://julialang.org/install/). +```julia +println("Hello, world!") +``` +- By default, Julia runs in Notebook Mode. You can turn this off in the settings. +- Plots with Plots.jl are embedded in the note by default. You can turn this off in the settings. Note that plots are only embedded when `display(current())` is called. +```julia +using Plots +x = range(0, 10, length=100); y = sin.(x); plot(x, y) +display(current()) +``` +- Note: the `ans` output from the last expression is always printed; this is a limitation of interacting with Julia over `stdin`. You can put expressions on the same line separated by semicolons (though the last expression's result will be printed unless you make it `nothing`), and you can also enclose the statements in a `let ... end` block (but note this creates a local scope): +```julia +using Plots +let + x = range(0, 10, length=100) + y = sin.(x) + plot(x, y) +end +display(current()) +``` +- Note: you will have to manually load the Plots package the first time (or any other package you use). You may want to put these in a separate block that you only execute on the first use: +```julia +import Pkg +Pkg.add("Plots") +``` + +
+
C++ @@ -757,7 +789,7 @@ print('should not run any pre blocks or global injects') ### Notebook Mode -A few languages (currently JS and Python) support *Notebook Mode*. If a language is using Notebook Mode (configurable in Settings), then all code blocks in a given file will execute in the same environment. +A few languages (currently JS, Python, and Julia) support *Notebook Mode*. If a language is using Notebook Mode (configurable in Settings), then all code blocks in a given file will execute in the same environment. Variables functions, etc. defined in one code block will be available in other code blocks. Code blocks are executed on demand; the order of code blocks in the file does not affect the order in which they are executed: diff --git a/src/RunButton.ts b/src/RunButton.ts index 04d7b147..ca6b64bc 100644 --- a/src/RunButton.ts +++ b/src/RunButton.ts @@ -76,7 +76,7 @@ async function handleExecution(block: CodeBlockContext) { case "zig": return runCode(s.zigPath, s.zigArgs, "zig", block, { shell: true }); case "ocaml": return runCode(s.ocamlPath, s.ocamlArgs, "ocaml", block, { shell: true }); case "php": return runCode(s.phpPath, s.phpArgs, s.phpFileExtension, block, { shell: true }); - case "julia": return runCode(s.juliaPath, s.juliaArgs,s.juliaFileExtension, block); + case "julia": return runCode(s.juliaPath, s.juliaArgs,s.juliaFileExtension, block, {transform: (code) => macro.expandJuliaPlot(code,s) }); case "latex": const outputPath: string = await retrieveFigurePath(block.srcCode, s.latexFigureTitlePattern, block.markdownFile, s); const invokeCompiler: string = [s.latexTexfotArgs, s.latexCompilerPath, s.latexCompilerArgs].join(" "); diff --git a/src/executors/JuliaExecutor.ts b/src/executors/JuliaExecutor.ts index 533386aa..3f086353 100644 --- a/src/executors/JuliaExecutor.ts +++ b/src/executors/JuliaExecutor.ts @@ -10,7 +10,7 @@ export default class JuliaExecutor extends ReplExecutor { constructor(settings: ExecutorSettings, file: string) { const args = settings.juliaArgs ? settings.juliaArgs.split(" ") : []; - args.unshift(`-i`); + //args.unshift(`--interactive`); super(settings, settings.juliaPath, args, file, "julia"); } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index c2c22aca..99ff2a0b 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -145,6 +145,7 @@ export interface ExecutorSettings { juliaPath: string; juliaArgs: string; juliaFileExtension: string; + juliaEmbedPlots: boolean; juliaInject: string; scalaPath: string; scalaArgs: string; @@ -394,6 +395,7 @@ export const DEFAULT_SETTINGS: ExecutorSettings = { phpInject: "", juliaPath: "julia", juliaArgs: "", + juliaEmbedPlots: true, juliaFileExtension: "jl", juliaInject: "", jsInteractive: true, diff --git a/src/settings/per-lang/makeJuliaSettings.ts b/src/settings/per-lang/makeJuliaSettings.ts index 343c8734..fc016171 100644 --- a/src/settings/per-lang/makeJuliaSettings.ts +++ b/src/settings/per-lang/makeJuliaSettings.ts @@ -3,6 +3,14 @@ import { SettingsTab } from "../SettingsTab"; export default (tab: SettingsTab, containerEl: HTMLElement) => { containerEl.createEl('h3', { text: 'Julia Settings' }); + new Setting(containerEl) + .setName('Embed Julia Plots') + .addToggle(toggle => toggle + .setValue(tab.plugin.settings.juliaEmbedPlots) + .onChange(async (value) => { + tab.plugin.settings.juliaEmbedPlots = value; + await tab.plugin.saveSettings(); + })); new Setting(containerEl) .setName('Julia path') .addText(text => text diff --git a/src/transforms/Magic.ts b/src/transforms/Magic.ts index 92bc3678..c55a0123 100644 --- a/src/transforms/Magic.ts +++ b/src/transforms/Magic.ts @@ -34,6 +34,7 @@ const PYTHON_PLOT_REGEX = /^(plt|matplotlib.pyplot|pyplot)\.show\(\)/gm; const R_PLOT_REGEX = /^plot\(.*\)/gm; const OCTAVE_PLOT_REGEX = /^plot\s*\(.*\);/gm; const MAXIMA_PLOT_REGEX = /^plot2d\s*\(.*\[.+\]\)\s*[$;]/gm; +const JULIA_PLOT_REGEX = /^display\(current\(\)\)/gm; /** * Parses the source code for the @vault command and replaces it with the vault path. @@ -291,3 +292,17 @@ export function expandMaximaPlot(source: string): string { return source; } + +export function expandJuliaPlot(source: string, settings: ExecutorSettings): string { + if (settings.juliaEmbedPlots) { + const matches = source.matchAll(JULIA_PLOT_REGEX); //replacing display(current()) + for (const match of matches) { + const tempFile = `${os.tmpdir()}/temp_${Date.now()}.png`.replace(/\\/g, "/").replace(/^\//, ""); + const substitute = `savefig("${tempFile}"); println(${JSON.stringify(`${TOGGLE_HTML_SIGIL}${TOGGLE_HTML_SIGIL}`)})`; + + source = source.replace(match[0], substitute); + } + } + return source; +} + From 203b6d1f88cfb698fc85abf58d47e3a5f07b04ff Mon Sep 17 00:00:00 2001 From: mdsw Date: Wed, 23 Jul 2025 17:06:20 -0400 Subject: [PATCH 5/5] update Julia README: let --> begin --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 152a1832..f41633a8 100644 --- a/README.md +++ b/README.md @@ -151,10 +151,10 @@ using Plots x = range(0, 10, length=100); y = sin.(x); plot(x, y) display(current()) ``` -- Note: the `ans` output from the last expression is always printed; this is a limitation of interacting with Julia over `stdin`. You can put expressions on the same line separated by semicolons (though the last expression's result will be printed unless you make it `nothing`), and you can also enclose the statements in a `let ... end` block (but note this creates a local scope): +- Note: the `ans` output from the last expression is always printed; this is a limitation of interacting with Julia over `stdin`. You can put expressions on the same line separated by semicolons (though the last expression's result will be printed unless you make it `nothing`), and you can also enclose the statements in a `begin ... end` block: ```julia using Plots -let +begin x = range(0, 10, length=100) y = sin.(x) plot(x, y)